diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 64d5d76..1bceb48 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,6 +24,6 @@ jobs: with: node-version: '14' - name: Install Flow CLI - run: bash -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" -- v1.5.0 + run: bash -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" - name: Run tests run: sh ./test.sh diff --git a/README.md b/README.md index 6015705..b75efde 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Currently, the list includes: | FTProviderFactory | 0x294e44e1ec6993c6 | 0xd8a7e05a7ac670c0 | | FTReceiverFactory | 0x294e44e1ec6993c6 | 0xd8a7e05a7ac670c0 | | NFTCollectionPublicFactory | 0x294e44e1ec6993c6 | 0xd8a7e05a7ac670c0 | -| NFTProviderAndCollectionPublicFactory | 0x294e44e1ec6993c6 | 0xd8a7e05a7ac670c0 | +| NFTProviderAndCollectionFactory | 0x294e44e1ec6993c6 | 0xd8a7e05a7ac670c0 | | NFTProviderFactory | 0x294e44e1ec6993c6 | 0xd8a7e05a7ac670c0 | | NFTCatalog | 0x49a7cda3a1eecc29 | 0x324c34e1c517e4db | | NFTCatalogAdmin | 0x49a7cda3a1eecc29 | 0x324c34e1c517e4db | diff --git a/contracts/Burner.cdc b/contracts/Burner.cdc new file mode 100644 index 0000000..0a3795e --- /dev/null +++ b/contracts/Burner.cdc @@ -0,0 +1,44 @@ +/// Burner is a contract that can facilitate the destruction of any resource on flow. +/// +/// Contributors +/// - Austin Kline - https://twitter.com/austin_flowty +/// - Deniz Edincik - https://twitter.com/bluesign +/// - Bastian Müller - https://twitter.com/turbolent +access(all) contract Burner { + /// When Crescendo (Cadence 1.0) is released, custom destructors will be removed from cadece. + /// Burnable is an interface meant to replace this lost feature, allowing anyone to add a callback + /// method to ensure they do not destroy something which is not meant to be, + /// or to add logic based on destruction such as tracking the supply of a FT Collection + /// + /// NOTE: The only way to see benefit from this interface + /// is to always use the burn method in this contract. Anyone who owns a resource can always elect **not** + /// to destroy a resource this way + access(all) resource interface Burnable { + access(contract) fun burnCallback() + } + + /// burn is a global method which will destroy any resource it is given. + /// If the provided resource implements the Burnable interface, + /// it will call the burnCallback method and then destroy afterwards. + access(all) fun burn(_ r: @AnyResource) { + if let s <- r as? @{Burnable} { + s.burnCallback() + destroy s + } else if let arr <- r as? @[AnyResource] { + while arr.length > 0 { + let item <- arr.removeFirst() + self.burn(<-item) + } + destroy arr + } else if let dict <- r as? @{HashableStruct: AnyResource} { + let keys = dict.keys + while keys.length > 0 { + let item <- dict.remove(key: keys.removeFirst())! + self.burn(<-item) + } + destroy dict + } else { + destroy r + } + } +} \ No newline at end of file diff --git a/contracts/FlowStorageFees.cdc b/contracts/FlowStorageFees.cdc index a93380e..4b0d70e 100644 --- a/contracts/FlowStorageFees.cdc +++ b/contracts/FlowStorageFees.cdc @@ -17,29 +17,29 @@ import "FungibleToken" import "FlowToken" -pub contract FlowStorageFees { +access(all) contract FlowStorageFees { // Emitted when the amount of storage capacity an account has per reserved Flow token changes - pub event StorageMegaBytesPerReservedFLOWChanged(_ storageMegaBytesPerReservedFLOW: UFix64) + access(all) event StorageMegaBytesPerReservedFLOWChanged(_ storageMegaBytesPerReservedFLOW: UFix64) // Emitted when the minimum amount of Flow tokens that an account needs to have reserved for storage capacity changes. - pub event MinimumStorageReservationChanged(_ minimumStorageReservation: UFix64) + access(all) event MinimumStorageReservationChanged(_ minimumStorageReservation: UFix64) // Defines how much storage capacity every account has per reserved Flow token. // definition is written per unit of flow instead of the inverse, // so there is no loss of precision calculating storage from flow, // but there is loss of precision when calculating flow per storage. - pub var storageMegaBytesPerReservedFLOW: UFix64 + access(all) var storageMegaBytesPerReservedFLOW: UFix64 // Defines the minimum amount of Flow tokens that every account needs to have reserved for storage capacity. // If an account has less then this amount reserved by the end of any transaction it participated in, the transaction will fail. - pub var minimumStorageReservation: UFix64 + access(all) var minimumStorageReservation: UFix64 // An administrator resource that can change the parameters of the FlowStorageFees smart contract. - pub resource Administrator { + access(all) resource Administrator { // Changes the amount of storage capacity an account has per accounts' reserved storage FLOW. - pub fun setStorageMegaBytesPerReservedFLOW(_ storageMegaBytesPerReservedFLOW: UFix64) { + access(all) fun setStorageMegaBytesPerReservedFLOW(_ storageMegaBytesPerReservedFLOW: UFix64) { if FlowStorageFees.storageMegaBytesPerReservedFLOW == storageMegaBytesPerReservedFLOW { return } @@ -48,7 +48,7 @@ pub contract FlowStorageFees { } // Changes the minimum amount of FLOW an account has to have reserved. - pub fun setMinimumStorageReservation(_ minimumStorageReservation: UFix64) { + access(all) fun setMinimumStorageReservation(_ minimumStorageReservation: UFix64) { if FlowStorageFees.minimumStorageReservation == minimumStorageReservation { return } @@ -63,19 +63,19 @@ pub contract FlowStorageFees { /// /// Returns megabytes /// If the account has no default balance it is counted as a balance of 0.0 FLOW - pub fun calculateAccountCapacity(_ accountAddress: Address): UFix64 { + access(all) fun calculateAccountCapacity(_ accountAddress: Address): UFix64 { var balance = 0.0 - if let balanceRef = getAccount(accountAddress) - .getCapability<&FlowToken.Vault{FungibleToken.Balance}>(/public/flowTokenBalance)! - .borrow() { - balance = balanceRef.balance + let acct = getAccount(accountAddress) + + if let balanceRef = acct.capabilities.borrow<&FlowToken.Vault>(/public/flowTokenBalance) { + balance = balanceRef.balance } return self.accountBalanceToAccountStorageCapacity(balance) } /// calculateAccountsCapacity returns the storage capacity of a batch of accounts - pub fun calculateAccountsCapacity(_ accountAddresses: [Address]): [UFix64] { + access(all) fun calculateAccountsCapacity(_ accountAddresses: [Address]): [UFix64] { let capacities: [UFix64] = [] for accountAddress in accountAddresses { let capacity = self.calculateAccountCapacity(accountAddress) @@ -88,19 +88,19 @@ pub contract FlowStorageFees { // This is used to check if a transaction will fail because of any account being over the storage capacity // The payer is an exception as its storage capacity is derived from its balance minus the maximum possible transaction fees // (transaction fees if the execution effort is at the execution efort limit, a.k.a.: computation limit, a.k.a.: gas limit) - pub fun getAccountsCapacityForTransactionStorageCheck(accountAddresses: [Address], payer: Address, maxTxFees: UFix64): [UFix64] { + access(all) fun getAccountsCapacityForTransactionStorageCheck(accountAddresses: [Address], payer: Address, maxTxFees: UFix64): [UFix64] { let capacities: [UFix64] = [] for accountAddress in accountAddresses { var balance = 0.0 - if let balanceRef = getAccount(accountAddress) - .getCapability<&FlowToken.Vault{FungibleToken.Balance}>(/public/flowTokenBalance)! - .borrow() { - if accountAddress == payer { - // if the account is the payer, deduct the maximum possible transaction fees from the balance - balance = balanceRef.balance.saturatingSubtract(maxTxFees) - } else { - balance = balanceRef.balance - } + let acct = getAccount(accountAddress) + + if let balanceRef = acct.capabilities.borrow<&FlowToken.Vault>(/public/flowTokenBalance) { + if accountAddress == payer { + // if the account is the payer, deduct the maximum possible transaction fees from the balance + balance = balanceRef.balance.saturatingSubtract(maxTxFees) + } else { + balance = balanceRef.balance + } } capacities.append(self.accountBalanceToAccountStorageCapacity(balance)) @@ -110,7 +110,7 @@ pub contract FlowStorageFees { // accountBalanceToAccountStorageCapacity returns the storage capacity // an account would have with given the flow balance of the account. - pub fun accountBalanceToAccountStorageCapacity(_ balance: UFix64): UFix64 { + access(all) view fun accountBalanceToAccountStorageCapacity(_ balance: UFix64): UFix64 { // get address token balance if balance < self.minimumStorageReservation { // if < then minimum return 0 @@ -123,15 +123,15 @@ pub contract FlowStorageFees { // Amount in Flow tokens // Returns megabytes - pub fun flowToStorageCapacity(_ amount: UFix64): UFix64 { + access(all) view fun flowToStorageCapacity(_ amount: UFix64): UFix64 { return amount.saturatingMultiply(FlowStorageFees.storageMegaBytesPerReservedFLOW) } // Amount in megabytes // Returns Flow tokens - pub fun storageCapacityToFlow(_ amount: UFix64): UFix64 { - if FlowStorageFees.storageMegaBytesPerReservedFLOW == 0.0 as UFix64 { - return 0.0 as UFix64 + access(all) view fun storageCapacityToFlow(_ amount: UFix64): UFix64 { + if FlowStorageFees.storageMegaBytesPerReservedFLOW == 0.0 { + return 0.0 } // possible loss of precision // putting the result back into `flowToStorageCapacity` might not yield the same result @@ -139,9 +139,9 @@ pub contract FlowStorageFees { } // converts storage used from UInt64 Bytes to UFix64 Megabytes. - pub fun convertUInt64StorageBytesToUFix64Megabytes(_ storage: UInt64): UFix64 { + access(all) view fun convertUInt64StorageBytesToUFix64Megabytes(_ storage: UInt64): UFix64 { // safe convert UInt64 to UFix64 (without overflow) - let f = UFix64(storage % 100000000 as UInt64) * 0.00000001 as UFix64 + UFix64(storage / 100000000 as UInt64) + let f = UFix64(storage % 100000000) * 0.00000001 + UFix64(storage / 100000000) // decimal point correction. Megabytes to bytes have a conversion of 10^-6 while UFix64 minimum value is 10^-8 let storageMb = f.saturatingMultiply(100.0) return storageMb @@ -151,13 +151,12 @@ pub contract FlowStorageFees { /// /// The available balance of an account is its default token balance minus what is reserved for storage. /// If the account has no default balance it is counted as a balance of 0.0 FLOW - pub fun defaultTokenAvailableBalance(_ accountAddress: Address): UFix64 { + access(all) fun defaultTokenAvailableBalance(_ accountAddress: Address): UFix64 { //get balance of account let acct = getAccount(accountAddress) var balance = 0.0 - if let balanceRef = acct - .getCapability(/public/flowTokenBalance) - .borrow<&FlowToken.Vault{FungibleToken.Balance}>() { + + if let balanceRef = acct.capabilities.borrow<&FlowToken.Vault>(/public/flowTokenBalance) { balance = balanceRef.balance } @@ -171,9 +170,9 @@ pub contract FlowStorageFees { /// /// The reserved balance of an account is its storage used multiplied by the storage cost per flow token. /// The reserved balance is at least the minimum storage reservation. - pub fun defaultTokenReservedBalance(_ accountAddress: Address): UFix64 { + access(all) view fun defaultTokenReservedBalance(_ accountAddress: Address): UFix64 { let acct = getAccount(accountAddress) - var reserved = self.storageCapacityToFlow(self.convertUInt64StorageBytesToUFix64Megabytes(acct.storageUsed)) + var reserved = self.storageCapacityToFlow(self.convertUInt64StorageBytesToUFix64Megabytes(acct.storage.used)) // at least self.minimumStorageReservation should be reserved if reserved < self.minimumStorageReservation { reserved = self.minimumStorageReservation @@ -187,7 +186,6 @@ pub contract FlowStorageFees { self.minimumStorageReservation = 0.0 // or 0 kb of minimum storage reservation let admin <- create Administrator() - self.account.save(<-admin, to: /storage/storageFeesAdmin) + self.account.storage.save(<-admin, to: /storage/storageFeesAdmin) } } - diff --git a/contracts/FlowToken.cdc b/contracts/FlowToken.cdc index 664d853..6e9914f 100644 --- a/contracts/FlowToken.cdc +++ b/contracts/FlowToken.cdc @@ -1,33 +1,27 @@ import "FungibleToken" import "MetadataViews" import "FungibleTokenMetadataViews" -import "ViewResolver" +import "Burner" -pub contract FlowToken: FungibleToken, ViewResolver { +access(all) contract FlowToken: FungibleToken { // Total supply of Flow tokens in existence - pub var totalSupply: UFix64 - - // Event that is emitted when the contract is created - pub event TokensInitialized(initialSupply: UFix64) + access(all) var totalSupply: UFix64 // Event that is emitted when tokens are withdrawn from a Vault - pub event TokensWithdrawn(amount: UFix64, from: Address?) + access(all) event TokensWithdrawn(amount: UFix64, from: Address?) // Event that is emitted when tokens are deposited to a Vault - pub event TokensDeposited(amount: UFix64, to: Address?) + access(all) event TokensDeposited(amount: UFix64, to: Address?) // Event that is emitted when new tokens are minted - pub event TokensMinted(amount: UFix64) - - // Event that is emitted when tokens are destroyed - pub event TokensBurned(amount: UFix64) + access(all) event TokensMinted(amount: UFix64) // Event that is emitted when a new minter resource is created - pub event MinterCreated(allowedAmount: UFix64) + access(all) event MinterCreated(allowedAmount: UFix64) // Event that is emitted when a new burner resource is created - pub event BurnerCreated() + access(all) event BurnerCreated() // Vault // @@ -41,16 +35,38 @@ pub contract FlowToken: FungibleToken, ViewResolver { // out of thin air. A special Minter resource needs to be defined to mint // new tokens. // - pub resource Vault: FungibleToken.Provider, FungibleToken.Receiver, FungibleToken.Balance, MetadataViews.Resolver { + access(all) resource Vault: FungibleToken.Vault { // holds the balance of a users tokens - pub var balance: UFix64 + access(all) var balance: UFix64 // initialize the balance at resource creation time init(balance: UFix64) { self.balance = balance } + /// Called when a fungible token is burned via the `Burner.burn()` method + access(contract) fun burnCallback() { + if self.balance > 0.0 { + FlowToken.totalSupply = FlowToken.totalSupply - self.balance + } + self.balance = 0.0 + } + + /// getSupportedVaultTypes optionally returns a list of vault types that this receiver accepts + access(all) view fun getSupportedVaultTypes(): {Type: Bool} { + return {self.getType(): true} + } + + access(all) view fun isSupportedVaultType(type: Type): Bool { + if (type == self.getType()) { return true } else { return false } + } + + /// Asks if the amount can be withdrawn from this vault + access(all) view fun isAvailableToWithdraw(amount: UFix64): Bool { + return amount <= self.balance + } + // withdraw // // Function that takes an integer amount as an argument @@ -60,7 +76,7 @@ pub contract FlowToken: FungibleToken, ViewResolver { // created Vault to the context that called so it can be deposited // elsewhere. // - pub fun withdraw(amount: UFix64): @FungibleToken.Vault { + access(FungibleToken.Withdraw) fun withdraw(amount: UFix64): @{FungibleToken.Vault} { self.balance = self.balance - amount emit TokensWithdrawn(amount: amount, from: self.owner?.address) return <-create Vault(balance: amount) @@ -73,7 +89,7 @@ pub contract FlowToken: FungibleToken, ViewResolver { // It is allowed to destroy the sent Vault because the Vault // was a temporary holder of the tokens. The Vault's balance has // been consumed and therefore can be destroyed. - pub fun deposit(from: @FungibleToken.Vault) { + access(all) fun deposit(from: @{FungibleToken.Vault}) { let vault <- from as! @FlowToken.Vault self.balance = self.balance + vault.balance emit TokensDeposited(amount: vault.balance, to: self.owner?.address) @@ -81,19 +97,13 @@ pub contract FlowToken: FungibleToken, ViewResolver { destroy vault } - destroy() { - if self.balance > 0.0 { - FlowToken.totalSupply = FlowToken.totalSupply - self.balance - } - } - /// Get all the Metadata Views implemented by FlowToken /// /// @return An array of Types defining the implemented views. This value will be used by /// developers to know which parameter to pass to the resolveView() method. /// - pub fun getViews(): [Type]{ - return FlowToken.getViews() + access(all) view fun getViews(): [Type]{ + return FlowToken.getContractViews(resourceType: nil) } /// Get a Metadata View from FlowToken @@ -101,8 +111,12 @@ pub contract FlowToken: FungibleToken, ViewResolver { /// @param view: The Type of the desired view. /// @return A structure representing the requested view. /// - pub fun resolveView(_ view: Type): AnyStruct? { - return FlowToken.resolveView(view) + access(all) fun resolveView(_ view: Type): AnyStruct? { + return FlowToken.resolveContractView(resourceType: nil, viewType: view) + } + + access(all) fun createEmptyVault(): @{FungibleToken.Vault} { + return <-create Vault(balance: 0.0) } } @@ -113,14 +127,16 @@ pub contract FlowToken: FungibleToken, ViewResolver { // and store the returned Vault in their storage in order to allow their // account to be able to receive deposits of this token type. // - pub fun createEmptyVault(): @FungibleToken.Vault { + access(all) fun createEmptyVault(vaultType: Type): @FlowToken.Vault { return <-create Vault(balance: 0.0) } - pub fun getViews(): [Type] { + /// Gets a list of the metadata views that this contract supports + access(all) view fun getContractViews(resourceType: Type?): [Type] { return [Type(), Type(), - Type()] + Type(), + Type()] } /// Get a Metadata View from FlowToken @@ -128,17 +144,17 @@ pub contract FlowToken: FungibleToken, ViewResolver { /// @param view: The Type of the desired view. /// @return A structure representing the requested view. /// - pub fun resolveView(_ view: Type): AnyStruct? { - switch view { + access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? { + switch viewType { case Type(): return FungibleTokenMetadataViews.FTView( - ftDisplay: self.resolveView(Type()) as! FungibleTokenMetadataViews.FTDisplay?, - ftVaultData: self.resolveView(Type()) as! FungibleTokenMetadataViews.FTVaultData? + ftDisplay: self.resolveContractView(resourceType: nil, viewType: Type()) as! FungibleTokenMetadataViews.FTDisplay?, + ftVaultData: self.resolveContractView(resourceType: nil, viewType: Type()) as! FungibleTokenMetadataViews.FTVaultData? ) case Type(): let media = MetadataViews.Media( file: MetadataViews.HTTPFile( - url: "https://assets.website-files.com/5f6294c0c7a8cdd643b1c820/5f6294c0c7a8cda55cb1c936_Flow_Wordmark.svg" + url: FlowToken.getLogoURI() ), mediaType: "image/svg+xml" ) @@ -146,7 +162,7 @@ pub contract FlowToken: FungibleToken, ViewResolver { return FungibleTokenMetadataViews.FTDisplay( name: "FLOW Network Token", symbol: "FLOW", - description: "FLOW is the protocol token that is required for transaction fees, storage fees, staking, and many applications built on the Flow Blockchain", + description: "FLOW is the native token for the Flow blockchain. It is required for securing the network, transaction fees, storage fees, staking, FLIP voting and may be used by applications built on the Flow Blockchain", externalURL: MetadataViews.ExternalURL("https://flow.com"), logos: medias, socials: { @@ -154,57 +170,50 @@ pub contract FlowToken: FungibleToken, ViewResolver { } ) case Type(): + let vaultRef = FlowToken.account.storage.borrow(from: /storage/flowTokenVault) + ?? panic("Could not borrow reference to the contract's Vault!") return FungibleTokenMetadataViews.FTVaultData( storagePath: /storage/flowTokenVault, receiverPath: /public/flowTokenReceiver, metadataPath: /public/flowTokenBalance, - providerPath: /private/flowTokenVault, - receiverLinkedType: Type<&FlowToken.Vault{FungibleToken.Receiver, FungibleToken.Balance, MetadataViews.Resolver}>(), - metadataLinkedType: Type<&FlowToken.Vault{FungibleToken.Balance, MetadataViews.Resolver}>(), - providerLinkedType: Type<&FlowToken.Vault{FungibleToken.Provider}>(), - createEmptyVaultFunction: (fun (): @FungibleToken.Vault { - return <-FlowToken.createEmptyVault() + receiverLinkedType: Type<&{FungibleToken.Receiver, FungibleToken.Vault}>(), + metadataLinkedType: Type<&{FungibleToken.Balance, FungibleToken.Vault}>(), + createEmptyVaultFunction: (fun (): @{FungibleToken.Vault} { + return <-vaultRef.createEmptyVault() }) ) + case Type(): + return FungibleTokenMetadataViews.TotalSupply(totalSupply: FlowToken.totalSupply) } return nil } - pub resource Administrator { + access(all) resource Administrator { // createNewMinter // // Function that creates and returns a new minter resource // - pub fun createNewMinter(allowedAmount: UFix64): @Minter { + access(all) fun createNewMinter(allowedAmount: UFix64): @Minter { emit MinterCreated(allowedAmount: allowedAmount) return <-create Minter(allowedAmount: allowedAmount) } - - // createNewBurner - // - // Function that creates and returns a new burner resource - // - pub fun createNewBurner(): @Burner { - emit BurnerCreated() - return <-create Burner() - } } // Minter // // Resource object that token admin accounts can hold to mint new tokens. // - pub resource Minter { + access(all) resource Minter { // the amount of tokens that the minter is allowed to mint - pub var allowedAmount: UFix64 + access(all) var allowedAmount: UFix64 // mintTokens // // Function that mints new tokens, adds them to the total supply, // and returns them to the calling context. // - pub fun mintTokens(amount: UFix64): @FlowToken.Vault { + access(all) fun mintTokens(amount: UFix64): @FlowToken.Vault { pre { amount > UFix64(0): "Amount minted must be greater than zero" amount <= self.allowedAmount: "Amount minted must be less than the allowed amount" @@ -220,55 +229,34 @@ pub contract FlowToken: FungibleToken, ViewResolver { } } - // Burner - // - // Resource object that token admin accounts can hold to burn tokens. - // - pub resource Burner { - - // burnTokens - // - // Function that destroys a Vault instance, effectively burning the tokens. - // - // Note: the burned tokens are automatically subtracted from the - // total supply in the Vault destructor. - // - pub fun burnTokens(from: @FungibleToken.Vault) { - let vault <- from as! @FlowToken.Vault - let amount = vault.balance - destroy vault - emit TokensBurned(amount: amount) - } + /// Gets the Flow Logo XML URI from storage + access(all) fun getLogoURI(): String { + return FlowToken.account.storage.copy(from: /storage/flowTokenLogoURI) ?? "" } - init(adminAccount: AuthAccount) { + init(adminAccount: auth(Storage, Capabilities) &Account) { self.totalSupply = 0.0 // Create the Vault with the total supply of tokens and save it in storage // let vault <- create Vault(balance: self.totalSupply) - adminAccount.save(<-vault, to: /storage/flowTokenVault) + + adminAccount.storage.save(<-vault, to: /storage/flowTokenVault) // Create a public capability to the stored Vault that only exposes // the `deposit` method through the `Receiver` interface // - adminAccount.link<&FlowToken.Vault{FungibleToken.Receiver, FungibleToken.Balance, MetadataViews.Resolver}>( - /public/flowTokenReceiver, - target: /storage/flowTokenVault - ) + let receiverCapability = adminAccount.capabilities.storage.issue<&FlowToken.Vault>(/storage/flowTokenVault) + adminAccount.capabilities.publish(receiverCapability, at: /public/flowTokenReceiver) // Create a public capability to the stored Vault that only exposes // the `balance` field through the `Balance` interface // - adminAccount.link<&FlowToken.Vault{FungibleToken.Balance, MetadataViews.Resolver}>( - /public/flowTokenBalance, - target: /storage/flowTokenVault - ) + let balanceCapability = adminAccount.capabilities.storage.issue<&FlowToken.Vault>(/storage/flowTokenVault) + adminAccount.capabilities.publish(balanceCapability, at: /public/flowTokenBalance) let admin <- create Administrator() - adminAccount.save(<-admin, to: /storage/flowTokenAdmin) + adminAccount.storage.save(<-admin, to: /storage/flowTokenAdmin) - // Emit an event that shows that the contract was initialized - emit TokensInitialized(initialSupply: self.totalSupply) } } diff --git a/contracts/FungibleToken.cdc b/contracts/FungibleToken.cdc index abc0581..9f377cb 100644 --- a/contracts/FungibleToken.cdc +++ b/contracts/FungibleToken.cdc @@ -2,21 +2,18 @@ # The Flow Fungible Token standard -## `FungibleToken` contract interface +## `FungibleToken` contract -The interface that all Fungible Token contracts would have to conform to. -If a users wants to deploy a new token contract, their contract -would need to implement the FungibleToken interface. - -Their contract would have to follow all the rules and naming -that the interface specifies. +The Fungible Token standard is no longer an interface +that all fungible token contracts would have to conform to. -## `Vault` resource +If a users wants to deploy a new token contract, their contract +does not need to implement the FungibleToken interface, but their tokens +do need to implement the interfaces defined in this contract. -Each account that owns tokens would need to have an instance -of the Vault resource stored in their account storage. +## `Vault` resource interface -The Vault resource has methods that the owner and other users can call. +Each fungible token resource type needs to implement the `Vault` resource interface. ## `Provider`, `Receiver`, and `Balance` resource interfaces @@ -32,32 +29,43 @@ these interfaces to do various things with the tokens. For example, a faucet can be implemented by conforming to the Provider interface. -By using resources and interfaces, users of Fungible Token contracts -can send and receive tokens peer-to-peer, without having to interact -with a central ledger smart contract. To send tokens to another user, -a user would simply withdraw the tokens from their Vault, then call -the deposit function on another user's Vault to complete the transfer. - */ -/// The interface that Fungible Token contracts implement. -/// -pub contract interface FungibleToken { +import "ViewResolver" +import "Burner" - /// The total number of tokens in existence. - /// It is up to the implementer to ensure that the total supply - /// stays accurate and up to date - pub var totalSupply: UFix64 +/// FungibleToken +/// +/// Fungible Token implementations are no longer required to implement the fungible token +/// interface. We still have it as an interface here because there are some useful +/// utility methods that many projects will still want to have on their contracts, +/// but they are by no means required. all that is required is that the token +/// implements the `Vault` interface +access(all) contract interface FungibleToken: ViewResolver { - /// The event that is emitted when the contract is created - pub event TokensInitialized(initialSupply: UFix64) + // An entitlement for allowing the withdrawal of tokens from a Vault + access(all) entitlement Withdraw /// The event that is emitted when tokens are withdrawn from a Vault - pub event TokensWithdrawn(amount: UFix64, from: Address?) + access(all) event Withdrawn(type: String, amount: UFix64, from: Address?, fromUUID: UInt64, withdrawnUUID: UInt64) + + /// The event that is emitted when tokens are deposited to a Vault + access(all) event Deposited(type: String, amount: UFix64, to: Address?, toUUID: UInt64, depositedUUID: UInt64) + + /// Event that is emitted when the global burn method is called with a non-zero balance + access(all) event Burned(type: String, amount: UFix64, fromUUID: UInt64) - /// The event that is emitted when tokens are deposited into a Vault - pub event TokensDeposited(amount: UFix64, to: Address?) + /// Balance + /// + /// The interface that provides standard functions\ + /// for getting balance information + /// + access(all) resource interface Balance { + access(all) var balance: UFix64 + } + /// Provider + /// /// The interface that enforces the requirements for withdrawing /// tokens from the implementing type. /// @@ -65,35 +73,37 @@ pub contract interface FungibleToken { /// because it leaves open the possibility of creating custom providers /// that do not necessarily need their own balance. /// - pub resource interface Provider { + access(all) resource interface Provider { + + /// Function to ask a provider if a specific amount of tokens + /// is available to be withdrawn + /// This could be useful to avoid panicing when calling withdraw + /// when the balance is unknown + /// Additionally, if the provider is pulling from multiple vaults + /// it only needs to check some of the vaults until the desired amount + /// is reached, potentially helping with performance. + /// + access(all) view fun isAvailableToWithdraw(amount: UFix64): Bool - /// Subtracts tokens from the owner's Vault + /// withdraw subtracts tokens from the implementing resource /// and returns a Vault with the removed tokens. /// - /// The function's access level is public, but this is not a problem - /// because only the owner storing the resource in their account - /// can initially call this function. - /// - /// The owner may grant other accounts access by creating a private - /// capability that allows specific other users to access - /// the provider resource through a reference. + /// The function's access level is `access(Withdraw)` + /// So in order to access it, one would either need the object itself + /// or an entitled reference with `Withdraw`. /// - /// The owner may also grant all accounts access by creating a public - /// capability that allows all users to access the provider - /// resource through a reference. - /// - /// @param amount: The amount of tokens to be withdrawn from the vault - /// @return The Vault resource containing the withdrawn funds - /// - pub fun withdraw(amount: UFix64): @Vault { + access(Withdraw) fun withdraw(amount: UFix64): @{Vault} { post { // `result` refers to the return value result.balance == amount: "Withdrawal amount must be the same as the balance of the withdrawn Vault" + emit Withdrawn(type: self.getType().identifier, amount: amount, from: self.owner?.address, fromUUID: self.uuid, withdrawnUUID: result.uuid) } } } + /// Receiver + /// /// The interface that enforces the requirements for depositing /// tokens into the implementing type. /// @@ -102,30 +112,55 @@ pub contract interface FungibleToken { /// can do custom things with the tokens, like split them up and /// send them to different places. /// - pub resource interface Receiver { + access(all) resource interface Receiver { - /// Takes a Vault and deposits it into the implementing resource type - /// - /// @param from: The Vault resource containing the funds that will be deposited + /// deposit takes a Vault and deposits it into the implementing resource type /// - pub fun deposit(from: @Vault) + access(all) fun deposit(from: @{Vault}) - /// Below is referenced from the FLIP #69 https://github.com/onflow/flips/blob/main/flips/20230206-fungible-token-vault-type-discovery.md - /// - /// Returns the dictionary of Vault types that the the receiver is able to accept in its `deposit` method - /// this then it would return `{Type<@FlowToken.Vault>(): true}` and if any custom receiver - /// uses the default implementation then it would return empty dictionary as its parent - /// resource doesn't conform with the `FungibleToken.Vault` resource. - /// - /// Custom receiver implementations are expected to upgrade their contracts to add an implementation - /// that supports this method because it is very valuable for various applications to have. - /// - /// @return dictionary of supported deposit vault types by the implementing resource. - /// - pub fun getSupportedVaultTypes(): {Type: Bool} { + /// getSupportedVaultTypes optionally returns a list of vault types that this receiver accepts + access(all) view fun getSupportedVaultTypes(): {Type: Bool} + + /// Returns whether or not the given type is accepted by the Receiver + /// A vault that can accept any type should just return true by default + access(all) view fun isSupportedVaultType(type: Type): Bool + } + + /// Vault + /// + /// Ideally, this interface would also conform to Receiver, Balance, Transferor, Provider, and Resolver + /// but that is not supported yet + /// + access(all) resource interface Vault: Receiver, Provider, Balance, ViewResolver.Resolver, Burner.Burnable { + + /// Field that tracks the balance of a vault + access(all) var balance: UFix64 + + /// Called when a fungible token is burned via the `Burner.burn()` method + /// Implementations can do any bookkeeping or emit any events + /// that should be emitted when a vault is destroyed. + /// Many implementations will want to update the token's total supply + /// to reflect that the tokens have been burned and removed from the supply. + /// Implementations also need to set the balance to zero before the end of the function + /// This is to prevent vault owners from spamming fake Burned events. + access(contract) fun burnCallback() { + pre { + emit Burned(type: self.getType().identifier, amount: self.balance, fromUUID: self.uuid) + } + post { + self.balance == 0.0: "The balance must be set to zero during the burnCallback method so that it cannot be spammed" + } + self.balance = 0.0 + } + + /// getSupportedVaultTypes optionally returns a list of vault types that this receiver accepts + /// The default implementation is included here because vaults are expected + /// to only accepted their own type, so they have no need to provide an implementation + /// for this function + access(all) view fun getSupportedVaultTypes(): {Type: Bool} { // Below check is implemented to make sure that run-time type would // only get returned when the parent resource conforms with `FungibleToken.Vault`. - if self.getType().isSubtype(of: Type<@FungibleToken.Vault>()) { + if self.getType().isSubtype(of: Type<@{FungibleToken.Vault}>()) { return {self.getType(): true} } else { // Return an empty dictionary as the default value for resource who don't @@ -133,104 +168,60 @@ pub contract interface FungibleToken { return {} } } - } - - /// The interface that contains the `balance` field of the Vault - /// and enforces that when new Vaults are created, the balance - /// is initialized correctly. - /// - pub resource interface Balance { - - /// The total balance of a vault - /// - pub var balance: UFix64 - - init(balance: UFix64) { - post { - self.balance == balance: - "Balance must be initialized to the initial balance" - } - } - - /// Function that returns all the Metadata Views implemented by a Fungible Token - /// - /// @return An array of Types defining the implemented views. This value will be used by - /// developers to know which parameter to pass to the resolveView() method. - /// - pub fun getViews(): [Type] { - return [] - } - /// Function that resolves a metadata view for this fungible token by type. - /// - /// @param view: The Type of the desired view. - /// @return A structure representing the requested view. - /// - pub fun resolveView(_ view: Type): AnyStruct? { - return nil + /// Checks if the given type is supported by this Vault + access(all) view fun isSupportedVaultType(type: Type): Bool { + return self.getSupportedVaultTypes()[type] ?? false } - } - - /// The resource that contains the functions to send and receive tokens. - /// The declaration of a concrete type in a contract interface means that - /// every Fungible Token contract that implements the FungibleToken interface - /// must define a concrete `Vault` resource that conforms to the `Provider`, `Receiver`, - /// and `Balance` interfaces, and declares their required fields and functions - /// - pub resource Vault: Provider, Receiver, Balance { - - /// The total balance of the vault - pub var balance: UFix64 - // The conforming type must declare an initializer - // that allows providing the initial balance of the Vault - // - init(balance: UFix64) - - /// Subtracts `amount` from the Vault's balance + /// withdraw subtracts `amount` from the Vault's balance /// and returns a new Vault with the subtracted balance /// - /// @param amount: The amount of tokens to be withdrawn from the vault - /// @return The Vault resource containing the withdrawn funds - /// - pub fun withdraw(amount: UFix64): @Vault { + access(Withdraw) fun withdraw(amount: UFix64): @{Vault} { pre { self.balance >= amount: "Amount withdrawn must be less than or equal than the balance of the Vault" } post { + result.getType() == self.getType(): "Must return the same vault type as self" // use the special function `before` to get the value of the `balance` field // at the beginning of the function execution // self.balance == before(self.balance) - amount: - "New Vault balance must be the difference of the previous balance and the withdrawn Vault" + "New Vault balance must be the difference of the previous balance and the withdrawn Vault balance" } } - /// Takes a Vault and deposits it into the implementing resource type + /// deposit takes a Vault and adds its balance to the balance of this Vault /// - /// @param from: The Vault resource containing the funds that will be deposited - /// - pub fun deposit(from: @Vault) { + access(all) fun deposit(from: @{FungibleToken.Vault}) { // Assert that the concrete type of the deposited vault is the same // as the vault that is accepting the deposit pre { from.isInstance(self.getType()): "Cannot deposit an incompatible token type" + emit Deposited(type: from.getType().identifier, amount: from.balance, to: self.owner?.address, toUUID: self.uuid, depositedUUID: from.uuid) } post { self.balance == before(self.balance) + before(from.balance): "New Vault balance must be the sum of the previous balance and the deposited Vault" } } + + /// createEmptyVault allows any user to create a new Vault that has a zero balance + /// + access(all) fun createEmptyVault(): @{Vault} { + post { + result.balance == 0.0: "The newly created Vault must have zero balance" + } + } } - /// Allows any user to create a new Vault that has a zero balance - /// - /// @return The new Vault resource + /// createEmptyVault allows any user to create a new Vault that has a zero balance /// - pub fun createEmptyVault(): @Vault { + access(all) fun createEmptyVault(vaultType: Type): @{FungibleToken.Vault} { post { + result.getType() == vaultType: "The returned vault does not match the desired type" result.balance == 0.0: "The newly created Vault must have zero balance" } } diff --git a/contracts/FungibleTokenMetadataViews.cdc b/contracts/FungibleTokenMetadataViews.cdc index f2b470b..c0cdd51 100644 --- a/contracts/FungibleTokenMetadataViews.cdc +++ b/contracts/FungibleTokenMetadataViews.cdc @@ -1,24 +1,26 @@ import "FungibleToken" import "MetadataViews" +import "ViewResolver" /// This contract implements the metadata standard proposed /// in FLIP-1087. /// -/// Ref: https://github.com/onflow/flow/blob/master/flips/20220811-fungible-tokens-metadata.md +/// Ref: https://github.com/onflow/flips/blob/main/application/20220811-fungible-tokens-metadata.md /// /// Structs and resources can implement one or more /// metadata types, called views. Each view type represents /// a different kind of metadata. /// -pub contract FungibleTokenMetadataViews { +access(all) contract FungibleTokenMetadataViews { + /// FTView wraps FTDisplay and FTVaultData, and is used to give a complete /// picture of a Fungible Token. Most Fungible Token contracts should /// implement this view. /// - pub struct FTView { - pub let ftDisplay: FTDisplay? - pub let ftVaultData: FTVaultData? - init( + access(all) struct FTView { + access(all) let ftDisplay: FTDisplay? + access(all) let ftVaultData: FTVaultData? + view init( ftDisplay: FTDisplay?, ftVaultData: FTVaultData? ) { @@ -32,7 +34,7 @@ pub contract FungibleTokenMetadataViews { /// @param viewResolver: A reference to the resolver resource /// @return A FTView struct /// - pub fun getFTView(viewResolver: &{MetadataViews.Resolver}): FTView { + access(all) fun getFTView(viewResolver: &{ViewResolver.Resolver}): FTView { let maybeFTView = viewResolver.resolveView(Type()) if let ftView = maybeFTView { return ftView as! FTView @@ -47,34 +49,34 @@ pub contract FungibleTokenMetadataViews { /// This can be used by applications to give an overview and /// graphics of the FT. /// - pub struct FTDisplay { + access(all) struct FTDisplay { /// The display name for this token. /// /// Example: "Flow" /// - pub let name: String + access(all) let name: String /// The abbreviated symbol for this token. /// /// Example: "FLOW" - pub let symbol: String + access(all) let symbol: String /// A description the provides an overview of this token. /// /// Example: "The FLOW token is the native currency of the Flow network." - pub let description: String + access(all) let description: String /// External link to a URL to view more information about the fungible token. - pub let externalURL: MetadataViews.ExternalURL + access(all) let externalURL: MetadataViews.ExternalURL /// One or more versions of the fungible token logo. - pub let logos: MetadataViews.Medias + access(all) let logos: MetadataViews.Medias /// Social links to reach the fungible token's social homepages. /// Possible keys may be "instagram", "twitter", "discord", etc. - pub let socials: {String: MetadataViews.ExternalURL} + access(all) let socials: {String: MetadataViews.ExternalURL} - init( + view init( name: String, symbol: String, description: String, @@ -96,7 +98,7 @@ pub contract FungibleTokenMetadataViews { /// @param viewResolver: A reference to the resolver resource /// @return An optional FTDisplay struct /// - pub fun getFTDisplay(_ viewResolver: &{MetadataViews.Resolver}): FTDisplay? { + access(all) fun getFTDisplay(_ viewResolver: &{ViewResolver.Resolver}): FTDisplay? { if let maybeDisplayView = viewResolver.resolveView(Type()) { if let displayView = maybeDisplayView as? FTDisplay { return displayView @@ -109,58 +111,45 @@ pub contract FungibleTokenMetadataViews { /// This can be used by applications to setup a FT vault with proper /// storage and public capabilities. /// - pub struct FTVaultData { + access(all) struct FTVaultData { /// Path in storage where this FT vault is recommended to be stored. - pub let storagePath: StoragePath + access(all) let storagePath: StoragePath /// Public path which must be linked to expose the public receiver capability. - pub let receiverPath: PublicPath + access(all) let receiverPath: PublicPath /// Public path which must be linked to expose the balance and resolver public capabilities. - pub let metadataPath: PublicPath - - /// Private path which should be linked to expose the provider capability to withdraw funds - /// from the vault. - pub let providerPath: PrivatePath + access(all) let metadataPath: PublicPath /// Type that should be linked at the `receiverPath`. This is a restricted type requiring /// the `FungibleToken.Receiver` interface. - pub let receiverLinkedType: Type + access(all) let receiverLinkedType: Type /// Type that should be linked at the `receiverPath`. This is a restricted type requiring - /// the `FungibleToken.Balance` and `MetadataViews.Resolver` interfaces. - pub let metadataLinkedType: Type - - /// Type that should be linked at the aforementioned private path. This - /// is normally a restricted type with at a minimum the `FungibleToken.Provider` interface. - pub let providerLinkedType: Type + /// the `ViewResolver.Resolver` interfaces. + access(all) let metadataLinkedType: Type /// Function that allows creation of an empty FT vault that is intended /// to store the funds. - pub let createEmptyVault: ((): @FungibleToken.Vault) + access(all) let createEmptyVault: fun(): @{FungibleToken.Vault} - init( + view init( storagePath: StoragePath, receiverPath: PublicPath, metadataPath: PublicPath, - providerPath: PrivatePath, receiverLinkedType: Type, metadataLinkedType: Type, - providerLinkedType: Type, - createEmptyVaultFunction: ((): @FungibleToken.Vault) + createEmptyVaultFunction: fun(): @{FungibleToken.Vault} ) { pre { receiverLinkedType.isSubtype(of: Type<&{FungibleToken.Receiver}>()): "Receiver public type must include FungibleToken.Receiver." - metadataLinkedType.isSubtype(of: Type<&{FungibleToken.Balance, MetadataViews.Resolver}>()): "Metadata public type must include FungibleToken.Balance and MetadataViews.Resolver interfaces." - providerLinkedType.isSubtype(of: Type<&{FungibleToken.Provider}>()): "Provider type must include FungibleToken.Provider interface." + metadataLinkedType.isSubtype(of: Type<&{FungibleToken.Vault}>()): "Metadata linked type must be a fungible token vault" } self.storagePath = storagePath self.receiverPath = receiverPath self.metadataPath = metadataPath - self.providerPath = providerPath self.receiverLinkedType = receiverLinkedType self.metadataLinkedType = metadataLinkedType - self.providerLinkedType = providerLinkedType self.createEmptyVault = createEmptyVaultFunction } } @@ -170,7 +159,7 @@ pub contract FungibleTokenMetadataViews { /// @param viewResolver: A reference to the resolver resource /// @return A optional FTVaultData struct /// - pub fun getFTVaultData(_ viewResolver: &{MetadataViews.Resolver}): FTVaultData? { + access(all) fun getFTVaultData(_ viewResolver: &{ViewResolver.Resolver}): FTVaultData? { if let view = viewResolver.resolveView(Type()) { if let v = view as? FTVaultData { return v @@ -179,5 +168,13 @@ pub contract FungibleTokenMetadataViews { return nil } + /// View to expose the total supply of the Vault's token + access(all) struct TotalSupply { + access(all) let supply: UFix64 + + view init(totalSupply: UFix64) { + self.supply = totalSupply + } + } } \ No newline at end of file diff --git a/contracts/FungibleTokenSwitchboard.cdc b/contracts/FungibleTokenSwitchboard.cdc new file mode 100644 index 0000000..23e1547 --- /dev/null +++ b/contracts/FungibleTokenSwitchboard.cdc @@ -0,0 +1,360 @@ +import "FungibleToken" + +/// The contract that allows an account to receive payments in multiple fungible +/// tokens using a single `{FungibleToken.Receiver}` capability. +/// This capability should ideally be stored at the +/// `FungibleTokenSwitchboard.ReceiverPublicPath = /public/GenericFTReceiver` +/// but it can be stored anywhere. +/// +access(all) contract FungibleTokenSwitchboard { + + // Storage and Public Paths + access(all) let StoragePath: StoragePath + access(all) let PublicPath: PublicPath + access(all) let ReceiverPublicPath: PublicPath + + access(all) entitlement Owner + + /// The event that is emitted when a new vault capability is added to a + /// switchboard resource. + /// + access(all) event VaultCapabilityAdded(type: Type, switchboardOwner: Address?, + capabilityOwner: Address?) + + /// The event that is emitted when a vault capability is removed from a + /// switchboard resource. + /// + access(all) event VaultCapabilityRemoved(type: Type, switchboardOwner: Address?, + capabilityOwner: Address?) + + /// The event that is emitted when a deposit can not be completed. + /// + access(all) event NotCompletedDeposit(type: Type, amount: UFix64, + switchboardOwner: Address?) + + /// The interface that enforces the method to allow anyone to check on the + /// available capabilities of a switchboard resource and also exposes the + /// deposit methods to deposit funds on it. + /// + access(all) resource interface SwitchboardPublic { + access(all) view fun getVaultTypesWithAddress(): {Type: Address} + access(all) view fun getSupportedVaultTypes(): {Type: Bool} + access(all) view fun isSupportedVaultType(type: Type): Bool + access(all) fun deposit(from: @{FungibleToken.Vault}) + access(all) fun safeDeposit(from: @{FungibleToken.Vault}): @{FungibleToken.Vault}? + access(all) view fun safeBorrowByType(type: Type): &{FungibleToken.Receiver}? + } + + /// The resource that stores the multiple fungible token receiver + /// capabilities, allowing the owner to add and remove them and anyone to + /// deposit any fungible token among the available types. + /// + access(all) resource Switchboard: FungibleToken.Receiver, SwitchboardPublic { + + /// Dictionary holding the fungible token receiver capabilities, + /// indexed by the fungible token vault type. + /// + access(contract) var receiverCapabilities: {Type: Capability<&{FungibleToken.Receiver}>} + + /// Adds a new fungible token receiver capability to the switchboard + /// resource. + /// + /// @param capability: The capability to expose a certain fungible + /// token vault deposit function through `{FungibleToken.Receiver}` that + /// will be added to the switchboard. + /// + access(Owner) fun addNewVault(capability: Capability<&{FungibleToken.Receiver}>) { + // Borrow a reference to the vault pointed to by the capability we + // want to store inside the switchboard + let vaultRef = capability.borrow() + ?? panic ("Cannot borrow reference to vault from capability") + // Check if there is a previous capability for this token, if not + if (self.receiverCapabilities[vaultRef.getType()] == nil) { + // use the vault reference type as key for storing the + // capability and then + self.receiverCapabilities[vaultRef.getType()] = capability + // emit the event that indicates that a new capability has been + // added + emit VaultCapabilityAdded(type: vaultRef.getType(), + switchboardOwner: self.owner?.address, + capabilityOwner: capability.address) + } else { + // If there was already a capability for that token, panic + panic("There is already a vault in the Switchboard for this token") + } + } + + /// Adds a number of new fungible token receiver capabilities by using + /// the paths where they are stored. + /// + /// @param paths: The paths where the public capabilities are stored. + /// @param address: The address of the owner of the capabilities. + /// + access(Owner) fun addNewVaultsByPath(paths: [PublicPath], address: Address) { + // Get the account where the public capabilities are stored + let owner = getAccount(address) + // For each path, get the saved capability and store it + // into the switchboard's receiver capabilities dictionary + for path in paths { + let capability = owner.capabilities.get<&{FungibleToken.Receiver}>(path) + // Borrow a reference to the vault pointed to by the capability + // we want to store inside the switchboard + // If the vault was borrowed successfully... + if let vaultRef = capability.borrow() { + // ...and if there is no previous capability added for that token + if (self.receiverCapabilities[vaultRef!.getType()] == nil) { + // Use the vault reference type as key for storing the + // capability + self.receiverCapabilities[vaultRef!.getType()] = capability + // and emit the event that indicates that a new + // capability has been added + emit VaultCapabilityAdded(type: vaultRef.getType(), + switchboardOwner: self.owner?.address, + capabilityOwner: address, + ) + } + } + } + } + + /// Adds a new fungible token receiver capability to the switchboard + /// resource specifying which `Type` of `@{FungibleToken.Vault}` can be + /// deposited to it. Use it to include in your switchboard "wrapper" + /// receivers such as a `@TokenForwarding.Forwarder`. It can also be + /// used to overwrite the type attached to a certain capability without + /// having to remove that capability first. + /// + /// @param capability: The capability to expose a certain fungible + /// token vault deposit function through `{FungibleToken.Receiver}` that + /// will be added to the switchboard. + /// + /// @param type: The type of fungible token that can be deposited to that + /// capability, rather than the `Type` from the reference borrowed from + /// said capability + /// + access(Owner) fun addNewVaultWrapper(capability: Capability<&{FungibleToken.Receiver}>, + type: Type) { + // Check if the capability is working + assert(capability.check(), message: "The passed capability is not valid") + // Use the type parameter as key for the capability + self.receiverCapabilities[type] = capability + // emit the event that indicates that a new capability has been + // added + emit VaultCapabilityAdded( + type: type, + switchboardOwner: self.owner?.address, + capabilityOwner: capability.address, + ) + } + + /// Adds zero or more new fungible token receiver capabilities to the + /// switchboard resource specifying which `Type`s of `@{FungibleToken.Vault}`s + /// can be deposited to it. Use it to include in your switchboard "wrapper" + /// receivers such as a `@TokenForwarding.Forwarder`. It can also be + /// used to overwrite the types attached to certain capabilities without + /// having to remove those capabilities first. + /// + /// @param paths: The paths where the public capabilities are stored. + /// @param types: The types of the fungible token to be deposited on each path. + /// @param address: The address of the owner of the capabilities. + /// + access(Owner) fun addNewVaultWrappersByPath(paths: [PublicPath], types: [Type], + address: Address) { + // Get the account where the public capabilities are stored + let owner = getAccount(address) + // For each path, get the saved capability and store it + // into the switchboard's receiver capabilities dictionary + for i, path in paths { + let capability = owner.capabilities.get<&{FungibleToken.Receiver}>(path) + // Borrow a reference to the vault pointed to by the capability + // we want to store inside the switchboard + // If the vault was borrowed successfully... + if let vaultRef = capability.borrow() { + // Use the vault reference type as key for storing the capability + self.receiverCapabilities[types[i]] = capability + // and emit the event that indicates that a new capability has been added + emit VaultCapabilityAdded( + type: types[i], + switchboardOwner: self.owner?.address, + capabilityOwner: address, + ) + } + } + } + + /// Removes a fungible token receiver capability from the switchboard + /// resource. + /// + /// @param capability: The capability to a fungible token vault to be + /// removed from the switchboard. + /// + access(Owner) fun removeVault(capability: Capability<&{FungibleToken.Receiver}>) { + // Borrow a reference to the vault pointed to by the capability we + // want to remove from the switchboard + let vaultRef = capability.borrow() + ?? panic ("Cannot borrow reference to vault from capability") + // Use the vault reference to find the capability to remove + self.receiverCapabilities.remove(key: vaultRef.getType()) + // Emit the event that indicates that a new capability has been + // removed + emit VaultCapabilityRemoved( + type: vaultRef.getType(), + switchboardOwner: self.owner?.address, + capabilityOwner: capability.address, + ) + } + + /// Takes a fungible token vault and routes it to the proper fungible + /// token receiver capability for depositing it. + /// + /// @param from: The deposited fungible token vault resource. + /// + access(all) fun deposit(from: @{FungibleToken.Vault}) { + // Get the capability from the ones stored at the switchboard + let depositedVaultCapability = self.receiverCapabilities[from.getType()] + ?? panic ("The deposited vault is not available on this switchboard") + + // Borrow the reference to the desired vault + let vaultRef = depositedVaultCapability.borrow() + ?? panic ("Can not borrow a reference to the the vault") + + vaultRef.deposit(from: <-from) + } + + /// Takes a fungible token vault and tries to route it to the proper + /// fungible token receiver capability for depositing the funds, + /// avoiding panicking if the vault is not available. + /// + /// @param vaultType: The type of the ft vault that wants to be + /// deposited. + /// + /// @return The deposited fungible token vault resource, without the + /// funds if the deposit was successful, or still containing the funds + /// if the reference to the needed vault was not found. + /// + access(all) fun safeDeposit(from: @{FungibleToken.Vault}): @{FungibleToken.Vault}? { + // Try to get the proper vault capability from the switchboard + // If the desired vault is present on the switchboard... + if let depositedVaultCapability = self.receiverCapabilities[from.getType()] { + // We try to borrow a reference to the vault from the capability + // If we can borrow a reference to the vault... + if let vaultRef = depositedVaultCapability.borrow() { + // We deposit the funds on said vault + vaultRef.deposit(from: <-from.withdraw(amount: from.balance)) + } + } + // if deposit failed for some reason + if from.balance > 0.0 { + emit NotCompletedDeposit( + type: from.getType(), + amount: from.balance, + switchboardOwner: self.owner?.address, + ) + return <-from + } + destroy from + return nil + } + + /// Checks that the capability tied to a type is valid + /// + /// @param vaultType: The type of the ft vault whose capability needs to be checked + /// + /// @return a boolean marking the capability for a type as valid or not + access(all) view fun checkReceiverByType(type: Type): Bool { + if self.receiverCapabilities[type] == nil { + return false + } + + return self.receiverCapabilities[type]!.check() + } + + /// Gets the receiver assigned to a provided vault type. + /// This is necessary because without it, it is not possible to look under the hood and see if a capability + /// is of an expected type or not. This helps guard against infinitely chained TokenForwarding or other invalid + /// malicious kinds of updates that could prevent listings from being made that are valid on storefronts. + /// + /// @param vaultType: The type of the ft vault whose capability needs to be checked + /// + /// @return an optional receiver capability for consumers of the switchboard to check/validate on their own + access(all) view fun safeBorrowByType(type: Type): &{FungibleToken.Receiver}? { + if !self.checkReceiverByType(type: type) { + return nil + } + + return self.receiverCapabilities[type]!.borrow() + } + + /// A getter function to know which tokens a certain switchboard + /// resource is prepared to receive along with the address where + /// those tokens will be deposited. + /// + /// @return A dictionary mapping the `{FungibleToken.Receiver}` + /// type to the receiver owner's address + /// + access(all) view fun getVaultTypesWithAddress(): {Type: Address} { + let effectiveTypesWithAddress: {Type: Address} = {} + // Check if each capability is live + for vaultType in self.receiverCapabilities.keys { + if self.receiverCapabilities[vaultType]!.check() { + // and attach it to the owner's address + effectiveTypesWithAddress[vaultType] = self.receiverCapabilities[vaultType]!.address + } + } + return effectiveTypesWithAddress + } + + /// A getter function that returns the token types supported by this resource, + /// which can be deposited using the 'deposit' function. + /// + /// @return Dictionary of FT types that can be deposited. + access(all) view fun getSupportedVaultTypes(): {Type: Bool} { + let supportedVaults: {Type: Bool} = {} + for receiverType in self.receiverCapabilities.keys { + if self.receiverCapabilities[receiverType]!.check() { + if receiverType.isSubtype(of: Type<@{FungibleToken.Vault}>()) { + supportedVaults[receiverType] = true + } + if receiverType.isSubtype(of: Type<@{FungibleToken.Receiver}>()) { + let receiverRef = self.receiverCapabilities[receiverType]!.borrow()! + let subReceiverSupportedTypes = receiverRef.getSupportedVaultTypes() + for subReceiverType in subReceiverSupportedTypes.keys { + if subReceiverType.isSubtype(of: Type<@{FungibleToken.Vault}>()) { + supportedVaults[subReceiverType] = true + } + } + } + } + } + return supportedVaults + } + + /// Returns whether or not the given type is accepted by the Receiver + /// A vault that can accept any type should just return true by default + access(all) view fun isSupportedVaultType(type: Type): Bool { + let supportedVaults = self.getSupportedVaultTypes() + if let supported = supportedVaults[type] { + return supported + } else { return false } + } + + init() { + // Initialize the capabilities dictionary + self.receiverCapabilities = {} + } + + } + + /// Function that allows to create a new blank switchboard. A user must call + /// this function and store the returned resource in their storage. + /// + access(all) fun createSwitchboard(): @Switchboard { + return <-create Switchboard() + } + + init() { + self.StoragePath = /storage/fungibleTokenSwitchboard + self.PublicPath = /public/fungibleTokenSwitchboardPublic + self.ReceiverPublicPath = /public/GenericFTReceiver + } +} \ No newline at end of file diff --git a/contracts/MetadataViews.cdc b/contracts/MetadataViews.cdc index 59c1927..3e42b02 100644 --- a/contracts/MetadataViews.cdc +++ b/contracts/MetadataViews.cdc @@ -1,5 +1,6 @@ import "FungibleToken" import "NonFungibleToken" +import "ViewResolver" /// This contract implements the metadata standard proposed /// in FLIP-0636. @@ -11,113 +12,38 @@ import "NonFungibleToken" /// a different kind of metadata, such as a creator biography /// or a JPEG image file. /// -pub contract MetadataViews { - - /// Provides access to a set of metadata views. A struct or - /// resource (e.g. an NFT) can implement this interface to provide access to - /// the views that it supports. - /// - pub resource interface Resolver { - pub fun getViews(): [Type] - pub fun resolveView(_ view: Type): AnyStruct? - } - - /// A group of view resolvers indexed by ID. - /// - pub resource interface ResolverCollection { - pub fun borrowViewResolver(id: UInt64): &{Resolver} - pub fun getIDs(): [UInt64] - } - - /// NFTView wraps all Core views along `id` and `uuid` fields, and is used - /// to give a complete picture of an NFT. Most NFTs should implement this - /// view. - /// - pub struct NFTView { - pub let id: UInt64 - pub let uuid: UInt64 - pub let display: Display? - pub let externalURL: ExternalURL? - pub let collectionData: NFTCollectionData? - pub let collectionDisplay: NFTCollectionDisplay? - pub let royalties: Royalties? - pub let traits: Traits? - - init( - id : UInt64, - uuid : UInt64, - display : Display?, - externalURL : ExternalURL?, - collectionData : NFTCollectionData?, - collectionDisplay : NFTCollectionDisplay?, - royalties : Royalties?, - traits: Traits? - ) { - self.id = id - self.uuid = uuid - self.display = display - self.externalURL = externalURL - self.collectionData = collectionData - self.collectionDisplay = collectionDisplay - self.royalties = royalties - self.traits = traits - } - } - - /// Helper to get an NFT view - /// - /// @param id: The NFT id - /// @param viewResolver: A reference to the resolver resource - /// @return A NFTView struct - /// - pub fun getNFTView(id: UInt64, viewResolver: &{Resolver}) : NFTView { - let nftView = viewResolver.resolveView(Type()) - if nftView != nil { - return nftView! as! NFTView - } - - return NFTView( - id : id, - uuid: viewResolver.uuid, - display: self.getDisplay(viewResolver), - externalURL : self.getExternalURL(viewResolver), - collectionData : self.getNFTCollectionData(viewResolver), - collectionDisplay : self.getNFTCollectionDisplay(viewResolver), - royalties : self.getRoyalties(viewResolver), - traits : self.getTraits(viewResolver) - ) - } +access(all) contract MetadataViews { /// Display is a basic view that includes the name, description and /// thumbnail for an object. Most objects should implement this view. /// - pub struct Display { + access(all) struct Display { /// The name of the object. /// /// This field will be displayed in lists and therefore should /// be short an concise. /// - pub let name: String + access(all) let name: String /// A written description of the object. /// /// This field will be displayed in a detailed view of the object, /// so can be more verbose (e.g. a paragraph instead of a single line). /// - pub let description: String + access(all) let description: String /// A small thumbnail representation of the object. /// /// This field should be a web-friendly file (i.e JPEG, PNG) /// that can be displayed in lists, link previews, etc. /// - pub let thumbnail: AnyStruct{File} + access(all) let thumbnail: {File} - init( + view init( name: String, description: String, - thumbnail: AnyStruct{File} + thumbnail: {File} ) { self.name = name self.description = description @@ -130,7 +56,7 @@ pub contract MetadataViews { /// @param viewResolver: A reference to the resolver resource /// @return An optional Display struct /// - pub fun getDisplay(_ viewResolver: &{Resolver}) : Display? { + access(all) fun getDisplay(_ viewResolver: &{ViewResolver.Resolver}) : Display? { if let view = viewResolver.resolveView(Type()) { if let v = view as? Display { return v @@ -142,20 +68,20 @@ pub contract MetadataViews { /// Generic interface that represents a file stored on or off chain. Files /// can be used to references images, videos and other media. /// - pub struct interface File { - pub fun uri(): String + access(all) struct interface File { + access(all) view fun uri(): String } /// View to expose a file that is accessible at an HTTP (or HTTPS) URL. /// - pub struct HTTPFile: File { - pub let url: String + access(all) struct HTTPFile: File { + access(all) let url: String - init(url: String) { + view init(url: String) { self.url = url } - pub fun uri(): String { + access(all) view fun uri(): String { return self.url } } @@ -165,13 +91,13 @@ pub contract MetadataViews { /// rather than a direct URI. A client application can use this CID /// to find and load the image via an IPFS gateway. /// - pub struct IPFSFile: File { + access(all) struct IPFSFile: File { /// CID is the content identifier for this IPFS file. /// /// Ref: https://docs.ipfs.io/concepts/content-addressing/ /// - pub let cid: String + access(all) let cid: String /// Path is an optional path to the file resource in an IPFS directory. /// @@ -179,9 +105,9 @@ pub contract MetadataViews { /// /// Ref: https://docs.ipfs.io/concepts/file-systems/ /// - pub let path: String? + access(all) let path: String? - init(cid: String, path: String?) { + view init(cid: String, path: String?) { self.cid = cid self.path = path } @@ -191,7 +117,7 @@ pub contract MetadataViews { /// /// @return The string containing the file uri /// - pub fun uri(): String { + access(all) view fun uri(): String { if let path = self.path { return "ipfs://".concat(self.cid).concat("/").concat(path) } @@ -200,125 +126,157 @@ pub contract MetadataViews { } } - /// Optional view for collections that issue multiple objects - /// with the same or similar metadata, for example an X of 100 set. This - /// information is useful for wallets and marketplaces. - /// An NFT might be part of multiple editions, which is why the edition - /// information is returned as an arbitrary sized array + /// A struct to represent a generic URI. May be used to represent the URI of + /// the NFT where the type of URI is not able to be determined (i.e. HTTP, + /// IPFS, etc.) /// - pub struct Edition { + access(all) struct URI: File { + /// The base URI prefix, if any. Not needed for all URIs, but helpful + /// for some use cases For example, updating a whole NFT collection's + /// image host easily + /// + access(all) let baseURI: String? + /// The URI string value + /// NOTE: this is set on init as a concatenation of the baseURI and the + /// value if baseURI != nil + /// + access(self) let value: String - /// The name of the edition - /// For example, this could be Set, Play, Series, - /// or any other way a project could classify its editions - pub let name: String? + access(all) view fun uri(): String { + return self.value + } - /// The edition number of the object. - /// For an "24 of 100 (#24/100)" item, the number is 24. - pub let number: UInt64 + init(baseURI: String?, value: String) { + self.baseURI = baseURI + self.value = baseURI != nil ? baseURI!.concat(value) : value + } + } - /// The max edition number of this type of objects. - /// This field should only be provided for limited-editioned objects. - /// For an "24 of 100 (#24/100)" item, max is 100. - /// For an item with unlimited edition, max should be set to nil. + access(all) struct Media { + + /// File for the media /// - pub let max: UInt64? + access(all) let file: {File} - init(name: String?, number: UInt64, max: UInt64?) { - if max != nil { - assert(number <= max!, message: "The number cannot be greater than the max number!") - } - self.name = name - self.number = number - self.max = max + /// media-type comes on the form of type/subtype as described here + /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types + /// + access(all) let mediaType: String + + view init(file: {File}, mediaType: String) { + self.file=file + self.mediaType=mediaType } } - /// Wrapper view for multiple Edition views + /// Wrapper view for multiple media views /// - pub struct Editions { + access(all) struct Medias { - /// An arbitrary-sized list for any number of editions - /// that the NFT might be a part of - pub let infoList: [Edition] + /// An arbitrary-sized list for any number of Media items + access(all) let items: [Media] - init(_ infoList: [Edition]) { - self.infoList = infoList + view init(_ items: [Media]) { + self.items = items } } - /// Helper to get Editions in a typesafe way + /// Helper to get Medias in a typesafe way /// /// @param viewResolver: A reference to the resolver resource - /// @return An optional Editions struct + /// @return A optional Medias struct /// - pub fun getEditions(_ viewResolver: &{Resolver}) : Editions? { - if let view = viewResolver.resolveView(Type()) { - if let v = view as? Editions { + access(all) fun getMedias(_ viewResolver: &{ViewResolver.Resolver}) : Medias? { + if let view = viewResolver.resolveView(Type()) { + if let v = view as? Medias { return v } } return nil } - /// View representing a project-defined serial number for a specific NFT - /// Projects have different definitions for what a serial number should be - /// Some may use the NFTs regular ID and some may use a different - /// classification system. The serial number is expected to be unique among - /// other NFTs within that project + /// View to represent a license according to https://spdx.org/licenses/ + /// This view can be used if the content of an NFT is licensed. /// - pub struct Serial { - pub let number: UInt64 + access(all) struct License { + access(all) let spdxIdentifier: String - init(_ number: UInt64) { - self.number = number + view init(_ identifier: String) { + self.spdxIdentifier = identifier } } - /// Helper to get Serial in a typesafe way + /// Helper to get License in a typesafe way /// /// @param viewResolver: A reference to the resolver resource - /// @return An optional Serial struct + /// @return An optional License struct /// - pub fun getSerial(_ viewResolver: &{Resolver}) : Serial? { - if let view = viewResolver.resolveView(Type()) { - if let v = view as? Serial { + access(all) fun getLicense(_ viewResolver: &{ViewResolver.Resolver}) : License? { + if let view = viewResolver.resolveView(Type()) { + if let v = view as? License { return v } } return nil } - /// View that defines the composable royalty standard that gives marketplaces a + /// View to expose a URL to this item on an external site. + /// This can be used by applications like .find and Blocto to direct users + /// to the original link for an NFT or a project page that describes the NFT collection. + /// eg https://www.my-nft-project.com/overview-of-nft-collection + /// + access(all) struct ExternalURL { + access(all) let url: String + + view init(_ url: String) { + self.url=url + } + } + + /// Helper to get ExternalURL in a typesafe way + /// + /// @param viewResolver: A reference to the resolver resource + /// @return An optional ExternalURL struct + /// + access(all) fun getExternalURL(_ viewResolver: &{ViewResolver.Resolver}) : ExternalURL? { + if let view = viewResolver.resolveView(Type()) { + if let v = view as? ExternalURL { + return v + } + } + return nil + } + + /// View that defines the composable royalty standard that gives marketplaces a /// unified interface to support NFT royalties. /// - pub struct Royalty { + access(all) struct Royalty { /// Generic FungibleToken Receiver for the beneficiary of the royalty /// Can get the concrete type of the receiver with receiver.getType() - /// Recommendation - Users should create a new link for a FlowToken - /// receiver for this using `getRoyaltyReceiverPublicPath()`, and not - /// use the default FlowToken receiver. This will allow users to update + /// Recommendation - Users should create a new link for a FlowToken + /// receiver for this using `getRoyaltyReceiverPublicPath()`, and not + /// use the default FlowToken receiver. This will allow users to update /// the capability in the future to use a more generic capability - pub let receiver: Capability<&AnyResource{FungibleToken.Receiver}> + access(all) let receiver: Capability<&{FungibleToken.Receiver}> - /// Multiplier used to calculate the amount of sale value transferred to - /// royalty receiver. Note - It should be between 0.0 and 1.0 - /// Ex - If the sale value is x and multiplier is 0.56 then the royalty + /// Multiplier used to calculate the amount of sale value transferred to + /// royalty receiver. Note - It should be between 0.0 and 1.0 + /// Ex - If the sale value is x and multiplier is 0.56 then the royalty /// value would be 0.56 * x. /// Generally percentage get represented in terms of basis points - /// in solidity based smart contracts while cadence offers `UFix64` - /// that already supports the basis points use case because its - /// operations are entirely deterministic integer operations and support + /// in solidity based smart contracts while cadence offers `UFix64` + /// that already supports the basis points use case because its + /// operations are entirely deterministic integer operations and support /// up to 8 points of precision. - pub let cut: UFix64 + access(all) let cut: UFix64 /// Optional description: This can be the cause of paying the royalty, /// the relationship between the `wallet` and the NFT, or anything else /// that the owner might want to specify. - pub let description: String + access(all) let description: String - init(receiver: Capability<&AnyResource{FungibleToken.Receiver}>, cut: UFix64, description: String) { + view init(receiver: Capability<&{FungibleToken.Receiver}>, cut: UFix64, description: String) { pre { cut >= 0.0 && cut <= 1.0 : "Cut value should be in valid range i.e [0,1]" } @@ -329,15 +287,15 @@ pub contract MetadataViews { } /// Wrapper view for multiple Royalty views. - /// Marketplaces can query this `Royalties` struct from NFTs + /// Marketplaces can query this `Royalties` struct from NFTs /// and are expected to pay royalties based on these specifications. /// - pub struct Royalties { + access(all) struct Royalties { /// Array that tracks the individual royalties access(self) let cutInfos: [Royalty] - pub init(_ cutInfos: [Royalty]) { + access(all) view init(_ cutInfos: [Royalty]) { // Validate that sum of all cut multipliers should not be greater than 1.0 var totalCut = 0.0 for royalty in cutInfos { @@ -352,7 +310,7 @@ pub contract MetadataViews { /// /// @return An array containing all the royalties structs /// - pub fun getRoyalties(): [Royalty] { + access(all) view fun getRoyalties(): [Royalty] { return self.cutInfos } } @@ -362,7 +320,7 @@ pub contract MetadataViews { /// @param viewResolver: A reference to the resolver resource /// @return A optional Royalties struct /// - pub fun getRoyalties(_ viewResolver: &{Resolver}) : Royalties? { + access(all) fun getRoyalties(_ viewResolver: &{ViewResolver.Resolver}) : Royalties? { if let view = viewResolver.resolveView(Type()) { if let v = view as? Royalties { return v @@ -377,162 +335,325 @@ pub contract MetadataViews { /// /// @return The PublicPath for the generic FT receiver /// - pub fun getRoyaltyReceiverPublicPath(): PublicPath { + access(all) view fun getRoyaltyReceiverPublicPath(): PublicPath { return /public/GenericFTReceiver } - /// View to represent, a file with an correspoiding mediaType. + /// View to represent a single field of metadata on an NFT. + /// This is used to get traits of individual key/value pairs along with some + /// contextualized data about the trait /// - pub struct Media { + access(all) struct Trait { + // The name of the trait. Like Background, Eyes, Hair, etc. + access(all) let name: String - /// File for the media + // The underlying value of the trait, the rest of the fields of a trait provide context to the value. + access(all) let value: AnyStruct + + // displayType is used to show some context about what this name and value represent + // for instance, you could set value to a unix timestamp, and specify displayType as "Date" to tell + // platforms to consume this trait as a date and not a number + access(all) let displayType: String? + + // Rarity can also be used directly on an attribute. + // + // This is optional because not all attributes need to contribute to the NFT's rarity. + access(all) let rarity: Rarity? + + view init(name: String, value: AnyStruct, displayType: String?, rarity: Rarity?) { + self.name = name + self.value = value + self.displayType = displayType + self.rarity = rarity + } + } + + /// Wrapper view to return all the traits on an NFT. + /// This is used to return traits as individual key/value pairs along with + /// some contextualized data about each trait. + access(all) struct Traits { + access(all) let traits: [Trait] + + view init(_ traits: [Trait]) { + self.traits = traits + } + + /// Adds a single Trait to the Traits view + /// + /// @param Trait: The trait struct to be added /// - pub let file: AnyStruct{File} + access(all) fun addTrait(_ t: Trait) { + self.traits.append(t) + } + } - /// media-type comes on the form of type/subtype as described here - /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types + /// Helper to get Traits view in a typesafe way + /// + /// @param viewResolver: A reference to the resolver resource + /// @return A optional Traits struct + /// + access(all) fun getTraits(_ viewResolver: &{ViewResolver.Resolver}) : Traits? { + if let view = viewResolver.resolveView(Type()) { + if let v = view as? Traits { + return v + } + } + return nil + } + + /// Helper function to easily convert a dictionary to traits. For NFT + /// collections that do not need either of the optional values of a Trait, + /// this method should suffice to give them an array of valid traits. + /// + /// @param dict: The dictionary to be converted to Traits + /// @param excludedNames: An optional String array specifying the `dict` + /// keys that are not wanted to become `Traits` + /// @return The generated Traits view + /// + access(all) fun dictToTraits(dict: {String: AnyStruct}, excludedNames: [String]?): Traits { + // Collection owners might not want all the fields in their metadata included. + // They might want to handle some specially, or they might just not want them included at all. + if excludedNames != nil { + for k in excludedNames! { + dict.remove(key: k) + } + } + + let traits: [Trait] = [] + for k in dict.keys { + let trait = Trait(name: k, value: dict[k]!, displayType: nil, rarity: nil) + traits.append(trait) + } + + return Traits(traits) + } + + /// Optional view for collections that issue multiple objects + /// with the same or similar metadata, for example an X of 100 set. This + /// information is useful for wallets and marketplaces. + /// An NFT might be part of multiple editions, which is why the edition + /// information is returned as an arbitrary sized array + /// + access(all) struct Edition { + + /// The name of the edition + /// For example, this could be Set, Play, Series, + /// or any other way a project could classify its editions + access(all) let name: String? + + /// The edition number of the object. + /// For an "24 of 100 (#24/100)" item, the number is 24. + access(all) let number: UInt64 + + /// The max edition number of this type of objects. + /// This field should only be provided for limited-editioned objects. + /// For an "24 of 100 (#24/100)" item, max is 100. + /// For an item with unlimited edition, max should be set to nil. /// - pub let mediaType: String + access(all) let max: UInt64? - init(file: AnyStruct{File}, mediaType: String) { - self.file=file - self.mediaType=mediaType + view init(name: String?, number: UInt64, max: UInt64?) { + if max != nil { + assert(number <= max!, message: "The number cannot be greater than the max number!") + } + self.name = name + self.number = number + self.max = max } } - /// Wrapper view for multiple media views + /// Wrapper view for multiple Edition views /// - pub struct Medias { + access(all) struct Editions { - /// An arbitrary-sized list for any number of Media items - pub let items: [Media] + /// An arbitrary-sized list for any number of editions + /// that the NFT might be a part of + access(all) let infoList: [Edition] - init(_ items: [Media]) { - self.items = items + view init(_ infoList: [Edition]) { + self.infoList = infoList } } - /// Helper to get Medias in a typesafe way + /// Helper to get Editions in a typesafe way /// /// @param viewResolver: A reference to the resolver resource - /// @return A optional Medias struct + /// @return An optional Editions struct /// - pub fun getMedias(_ viewResolver: &{Resolver}) : Medias? { - if let view = viewResolver.resolveView(Type()) { - if let v = view as? Medias { + access(all) fun getEditions(_ viewResolver: &{ViewResolver.Resolver}) : Editions? { + if let view = viewResolver.resolveView(Type()) { + if let v = view as? Editions { return v } } return nil } - /// View to represent a license according to https://spdx.org/licenses/ - /// This view can be used if the content of an NFT is licensed. + /// View representing a project-defined serial number for a specific NFT + /// Projects have different definitions for what a serial number should be + /// Some may use the NFTs regular ID and some may use a different + /// classification system. The serial number is expected to be unique among + /// other NFTs within that project /// - pub struct License { - pub let spdxIdentifier: String + access(all) struct Serial { + access(all) let number: UInt64 - init(_ identifier: String) { - self.spdxIdentifier = identifier + view init(_ number: UInt64) { + self.number = number } } - /// Helper to get License in a typesafe way + /// Helper to get Serial in a typesafe way /// /// @param viewResolver: A reference to the resolver resource - /// @return A optional License struct + /// @return An optional Serial struct /// - pub fun getLicense(_ viewResolver: &{Resolver}) : License? { - if let view = viewResolver.resolveView(Type()) { - if let v = view as? License { + access(all) fun getSerial(_ viewResolver: &{ViewResolver.Resolver}) : Serial? { + if let view = viewResolver.resolveView(Type()) { + if let v = view as? Serial { return v } } return nil } - /// View to expose a URL to this item on an external site. - /// This can be used by applications like .find and Blocto to direct users - /// to the original link for an NFT or a project page that describes the NFT collection. - /// eg https://www.my-nft-project.com/overview-of-nft-collection + /// View to expose rarity information for a single rarity + /// Note that a rarity needs to have either score or description but it can + /// have both /// - pub struct ExternalURL { - pub let url: String + access(all) struct Rarity { + /// The score of the rarity as a number + access(all) let score: UFix64? - init(_ url: String) { - self.url=url + /// The maximum value of score + access(all) let max: UFix64? + + /// The description of the rarity as a string. + /// + /// This could be Legendary, Epic, Rare, Uncommon, Common or any other string value + access(all) let description: String? + + view init(score: UFix64?, max: UFix64?, description: String?) { + if score == nil && description == nil { + panic("A Rarity needs to set score, description or both") + } + + self.score = score + self.max = max + self.description = description } } - /// Helper to get ExternalURL in a typesafe way + /// Helper to get Rarity view in a typesafe way /// /// @param viewResolver: A reference to the resolver resource - /// @return A optional ExternalURL struct + /// @return A optional Rarity struct /// - pub fun getExternalURL(_ viewResolver: &{Resolver}) : ExternalURL? { - if let view = viewResolver.resolveView(Type()) { - if let v = view as? ExternalURL { + access(all) fun getRarity(_ viewResolver: &{ViewResolver.Resolver}) : Rarity? { + if let view = viewResolver.resolveView(Type()) { + if let v = view as? Rarity { return v } } return nil } + /// NFTView wraps all Core views along `id` and `uuid` fields, and is used + /// to give a complete picture of an NFT. Most NFTs should implement this + /// view. + /// + access(all) struct NFTView { + access(all) let id: UInt64 + access(all) let uuid: UInt64 + access(all) let display: MetadataViews.Display? + access(all) let externalURL: MetadataViews.ExternalURL? + access(all) let collectionData: NFTCollectionData? + access(all) let collectionDisplay: NFTCollectionDisplay? + access(all) let royalties: Royalties? + access(all) let traits: Traits? + + view init( + id : UInt64, + uuid : UInt64, + display : MetadataViews.Display?, + externalURL : MetadataViews.ExternalURL?, + collectionData : NFTCollectionData?, + collectionDisplay : NFTCollectionDisplay?, + royalties : Royalties?, + traits: Traits? + ) { + self.id = id + self.uuid = uuid + self.display = display + self.externalURL = externalURL + self.collectionData = collectionData + self.collectionDisplay = collectionDisplay + self.royalties = royalties + self.traits = traits + } + } + + /// Helper to get an NFT view + /// + /// @param id: The NFT id + /// @param viewResolver: A reference to the resolver resource + /// @return A NFTView struct + /// + access(all) fun getNFTView(id: UInt64, viewResolver: &{ViewResolver.Resolver}) : NFTView { + let nftView = viewResolver.resolveView(Type()) + if nftView != nil { + return nftView! as! NFTView + } + + return NFTView( + id : id, + uuid: viewResolver.uuid, + display: MetadataViews.getDisplay(viewResolver), + externalURL : MetadataViews.getExternalURL(viewResolver), + collectionData : self.getNFTCollectionData(viewResolver), + collectionDisplay : self.getNFTCollectionDisplay(viewResolver), + royalties : self.getRoyalties(viewResolver), + traits : self.getTraits(viewResolver) + ) + } + /// View to expose the information needed store and retrieve an NFT. - /// This can be used by applications to setup a NFT collection with proper + /// This can be used by applications to setup a NFT collection with proper /// storage and public capabilities. /// - pub struct NFTCollectionData { + access(all) struct NFTCollectionData { /// Path in storage where this NFT is recommended to be stored. - pub let storagePath: StoragePath + access(all) let storagePath: StoragePath /// Public path which must be linked to expose public capabilities of this NFT /// including standard NFT interfaces and metadataviews interfaces - pub let publicPath: PublicPath + access(all) let publicPath: PublicPath - /// Private path which should be linked to expose the provider - /// capability to withdraw NFTs from the collection holding NFTs - pub let providerPath: PrivatePath + /// The concrete type of the collection that is exposed to the public + /// now that entitlements exist, it no longer needs to be restricted to a specific interface + access(all) let publicCollection: Type - /// Public collection type that is expected to provide sufficient read-only access to standard - /// functions (deposit + getIDs + borrowNFT) - /// This field is for backwards compatibility with collections that have not used the standard - /// NonFungibleToken.CollectionPublic interface when setting up collections. For new - /// collections, this may be set to be equal to the type specified in `publicLinkedType`. - pub let publicCollection: Type - - /// Type that should be linked at the aforementioned public path. This is normally a - /// restricted type with many interfaces. Notably the `NFT.CollectionPublic`, - /// `NFT.Receiver`, and `MetadataViews.ResolverCollection` interfaces are required. - pub let publicLinkedType: Type - - /// Type that should be linked at the aforementioned private path. This is normally - /// a restricted type with at a minimum the `NFT.Provider` interface - pub let providerLinkedType: Type + /// Type that should be linked at the aforementioned public path + access(all) let publicLinkedType: Type /// Function that allows creation of an empty NFT collection that is intended to store /// this NFT. - pub let createEmptyCollection: ((): @NonFungibleToken.Collection) + access(all) let createEmptyCollection: fun(): @{NonFungibleToken.Collection} - init( + view init( storagePath: StoragePath, publicPath: PublicPath, - providerPath: PrivatePath, publicCollection: Type, publicLinkedType: Type, - providerLinkedType: Type, - createEmptyCollectionFunction: ((): @NonFungibleToken.Collection) + createEmptyCollectionFunction: fun(): @{NonFungibleToken.Collection} ) { pre { - publicLinkedType.isSubtype(of: Type<&{NonFungibleToken.CollectionPublic, NonFungibleToken.Receiver, MetadataViews.ResolverCollection}>()): "Public type must include NonFungibleToken.CollectionPublic, NonFungibleToken.Receiver, and MetadataViews.ResolverCollection interfaces." - providerLinkedType.isSubtype(of: Type<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic, MetadataViews.ResolverCollection}>()): "Provider type must include NonFungibleToken.Provider, NonFungibleToken.CollectionPublic, and MetadataViews.ResolverCollection interface." + publicLinkedType.isSubtype(of: Type<&{NonFungibleToken.Collection}>()): "Public type must be a subtype of NonFungibleToken.Collection interface." } self.storagePath=storagePath self.publicPath=publicPath - self.providerPath = providerPath self.publicCollection=publicCollection self.publicLinkedType=publicLinkedType - self.providerLinkedType = providerLinkedType self.createEmptyCollection=createEmptyCollectionFunction } } @@ -542,7 +663,7 @@ pub contract MetadataViews { /// @param viewResolver: A reference to the resolver resource /// @return A optional NFTCollectionData struct /// - pub fun getNFTCollectionData(_ viewResolver: &{Resolver}) : NFTCollectionData? { + access(all) fun getNFTCollectionData(_ viewResolver: &{ViewResolver.Resolver}) : NFTCollectionData? { if let view = viewResolver.resolveView(Type()) { if let v = view as? NFTCollectionData { return v @@ -552,36 +673,36 @@ pub contract MetadataViews { } /// View to expose the information needed to showcase this NFT's - /// collection. This can be used by applications to give an overview and + /// collection. This can be used by applications to give an overview and /// graphics of the NFT collection this NFT belongs to. /// - pub struct NFTCollectionDisplay { + access(all) struct NFTCollectionDisplay { // Name that should be used when displaying this NFT collection. - pub let name: String + access(all) let name: String // Description that should be used to give an overview of this collection. - pub let description: String + access(all) let description: String // External link to a URL to view more information about this collection. - pub let externalURL: ExternalURL + access(all) let externalURL: MetadataViews.ExternalURL // Square-sized image to represent this collection. - pub let squareImage: Media + access(all) let squareImage: MetadataViews.Media - // Banner-sized image for this collection, recommended to have a size near 1200x630. - pub let bannerImage: Media + // Banner-sized image for this collection, recommended to have a size near 1400x350. + access(all) let bannerImage: MetadataViews.Media // Social links to reach this collection's social homepages. // Possible keys may be "instagram", "twitter", "discord", etc. - pub let socials: {String: ExternalURL} + access(all) let socials: {String: MetadataViews.ExternalURL} - init( + view init( name: String, description: String, - externalURL: ExternalURL, - squareImage: Media, - bannerImage: Media, - socials: {String: ExternalURL} + externalURL: MetadataViews.ExternalURL, + squareImage: MetadataViews.Media, + bannerImage: MetadataViews.Media, + socials: {String: MetadataViews.ExternalURL} ) { self.name = name self.description = description @@ -592,13 +713,13 @@ pub contract MetadataViews { } } - /// Helper to get NFTCollectionDisplay in a way that will return a typed + /// Helper to get NFTCollectionDisplay in a way that will return a typed /// Optional /// /// @param viewResolver: A reference to the resolver resource /// @return A optional NFTCollection struct /// - pub fun getNFTCollectionDisplay(_ viewResolver: &{Resolver}) : NFTCollectionDisplay? { + access(all) fun getNFTCollectionDisplay(_ viewResolver: &{ViewResolver.Resolver}) : NFTCollectionDisplay? { if let view = viewResolver.resolveView(Type()) { if let v = view as? NFTCollectionDisplay { return v @@ -606,135 +727,53 @@ pub contract MetadataViews { } return nil } - - /// View to expose rarity information for a single rarity - /// Note that a rarity needs to have either score or description but it can - /// have both + /// This view may be used by Cadence-native projects to define their + /// contract- and token-level metadata according to EVM-compatible formats. + /// Several ERC standards (e.g. ERC20, ERC721, etc.) expose name and symbol + /// values to define assets as well as contract- & token-level metadata view + /// `tokenURI(uint256)` and `contractURI()` methods. This view enables + /// Cadence projects to define in their own contracts how they would like + /// their metadata to be defined when bridged to EVM. /// - pub struct Rarity { - /// The score of the rarity as a number - pub let score: UFix64? + access(all) struct EVMBridgedMetadata { - /// The maximum value of score - pub let max: UFix64? - - /// The description of the rarity as a string. + /// The name of the asset /// - /// This could be Legendary, Epic, Rare, Uncommon, Common or any other string value - pub let description: String? - - init(score: UFix64?, max: UFix64?, description: String?) { - if score == nil && description == nil { - panic("A Rarity needs to set score, description or both") - } - - self.score = score - self.max = max - self.description = description - } - } - - /// Helper to get Rarity view in a typesafe way - /// - /// @param viewResolver: A reference to the resolver resource - /// @return A optional Rarity struct - /// - pub fun getRarity(_ viewResolver: &{Resolver}) : Rarity? { - if let view = viewResolver.resolveView(Type()) { - if let v = view as? Rarity { - return v - } - } - return nil - } - - /// View to represent a single field of metadata on an NFT. - /// This is used to get traits of individual key/value pairs along with some - /// contextualized data about the trait - /// - pub struct Trait { - // The name of the trait. Like Background, Eyes, Hair, etc. - pub let name: String - - // The underlying value of the trait, the rest of the fields of a trait provide context to the value. - pub let value: AnyStruct - - // displayType is used to show some context about what this name and value represent - // for instance, you could set value to a unix timestamp, and specify displayType as "Date" to tell - // platforms to consume this trait as a date and not a number - pub let displayType: String? - - // Rarity can also be used directly on an attribute. - // - // This is optional because not all attributes need to contribute to the NFT's rarity. - pub let rarity: Rarity? - - init(name: String, value: AnyStruct, displayType: String?, rarity: Rarity?) { - self.name = name - self.value = value - self.displayType = displayType - self.rarity = rarity - } - } - - /// Wrapper view to return all the traits on an NFT. - /// This is used to return traits as individual key/value pairs along with - /// some contextualized data about each trait. - pub struct Traits { - pub let traits: [Trait] + access(all) let name: String - init(_ traits: [Trait]) { - self.traits = traits - } + /// The symbol of the asset + /// + access(all) let symbol: String - /// Adds a single Trait to the Traits view + /// The URI of the asset - this can either be contract-level or + /// token-level URI depending on where the metadata is resolved. It + /// is recommended to reference EVM metadata standards for how to best + /// prepare your view's formatted value. /// - /// @param Trait: The trait struct to be added + /// For example, while you may choose to take advantage of onchain + /// metadata, as is the case for most Cadence NFTs, you may also choose + /// to represent your asset's metadata in IPFS and assign this value as + /// an IPFSFile struct pointing to that IPFS file. Alternatively, you + /// may serialize your NFT's metadata and assign it as a JSON string + /// data URL representating the NFT's onchain metadata at the time this + /// view is resolved. /// - pub fun addTrait(_ t: Trait) { - self.traits.append(t) + access(all) let uri: {File} + + init(name: String, symbol: String, uri: {File}) { + self.name = name + self.symbol = symbol + self.uri = uri } } - /// Helper to get Traits view in a typesafe way - /// - /// @param viewResolver: A reference to the resolver resource - /// @return A optional Traits struct - /// - pub fun getTraits(_ viewResolver: &{Resolver}) : Traits? { - if let view = viewResolver.resolveView(Type()) { - if let v = view as? Traits { + access(all) fun getEVMBridgedMetadata(_ viewResolver: &{ViewResolver.Resolver}) : EVMBridgedMetadata? { + if let view = viewResolver.resolveView(Type()) { + if let v = view as? EVMBridgedMetadata { return v } } return nil } - /// Helper function to easily convert a dictionary to traits. For NFT - /// collections that do not need either of the optional values of a Trait, - /// this method should suffice to give them an array of valid traits. - /// - /// @param dict: The dictionary to be converted to Traits - /// @param excludedNames: An optional String array specifying the `dict` - /// keys that are not wanted to become `Traits` - /// @return The generated Traits view - /// - pub fun dictToTraits(dict: {String: AnyStruct}, excludedNames: [String]?): Traits { - // Collection owners might not want all the fields in their metadata included. - // They might want to handle some specially, or they might just not want them included at all. - if excludedNames != nil { - for k in excludedNames! { - dict.remove(key: k) - } - } - - let traits: [Trait] = [] - for k in dict.keys { - let trait = Trait(name: k, value: dict[k]!, displayType: nil, rarity: nil) - traits.append(trait) - } - - return Traits(traits) - } - -} +} \ No newline at end of file diff --git a/contracts/NonFungibleToken.cdc b/contracts/NonFungibleToken.cdc index 5ebd8fc..d7d2e6b 100644 --- a/contracts/NonFungibleToken.cdc +++ b/contracts/NonFungibleToken.cdc @@ -2,20 +2,17 @@ ## The Flow Non-Fungible Token standard -## `NonFungibleToken` contract interface +## `NonFungibleToken` contract -The interface that all Non-Fungible Token contracts could conform to. -If a user wants to deploy a new NFT contract, their contract would need -to implement the NonFungibleToken interface. +The interface that all Non-Fungible Token contracts should conform to. +If a user wants to deploy a new NFT contract, their contract should implement +The types defined here -Their contract would have to follow all the rules and naming -that the interface specifies. - -## `NFT` resource +## `NFT` resource interface The core resource type that represents an NFT in the smart contract. -## `Collection` Resource +## `Collection` Resource interface The resource that stores a user's NFT collection. It includes a few functions to allow the owner to easily @@ -26,10 +23,8 @@ move tokens in and out of the collection. These interfaces declare functions with some pre and post conditions that require the Collection to follow certain naming and behavior standards. -They are separate because it gives the user the ability to share a reference -to their Collection that only exposes the fields and functions in one or more -of the interfaces. It also gives users the ability to make custom resources -that implement these interfaces to do various things with the tokens. +They are separate because it gives developers the ability to define functions +that can use any type that implements these interfaces By using resources and interfaces, users of NFT smart contracts can send and receive tokens peer-to-peer, without having to interact with a central ledger @@ -41,162 +36,206 @@ Collection to complete the transfer. */ -/// The main NFT contract interface. Other NFT contracts will -/// import and implement this interface +import "ViewResolver" + +/// The main NFT contract. Other NFT contracts will +/// import and implement the interfaces defined in this contract /// -pub contract interface NonFungibleToken { +access(all) contract interface NonFungibleToken: ViewResolver { - /// The total number of tokens of this type in existence - pub var totalSupply: UInt64 + /// An entitlement for allowing the withdrawal of tokens from a Vault + access(all) entitlement Withdraw - /// Event that emitted when the NFT contract is initialized + /// An entitlement for allowing updates and update events for an NFT + access(all) entitlement Update + + /// Event that contracts should emit when the metadata of an NFT is updated + /// It can only be emitted by calling the `emitNFTUpdated` function + /// with an `Updatable` entitled reference to the NFT that was updated + /// The entitlement prevents spammers from calling this from other users' collections + /// because only code within a collection or that has special entitled access + /// to the collections methods will be able to get the entitled reference + /// + /// The event makes it so that third-party indexers can monitor the events + /// and query the updated metadata from the owners' collections. /// - pub event ContractInitialized() + access(all) event Updated(type: String, id: UInt64, uuid: UInt64, owner: Address?) + access(all) view fun emitNFTUpdated(_ nftRef: auth(Update) &{NonFungibleToken.NFT}) + { + emit Updated(type: nftRef.getType().identifier, id: nftRef.id, uuid: nftRef.uuid, owner: nftRef.owner?.address) + } + /// Event that is emitted when a token is withdrawn, - /// indicating the owner of the collection that it was withdrawn from. + /// indicating the type, id, uuid, the owner of the collection that it was withdrawn from, + /// and the UUID of the resource it was withdrawn from, usually a collection. /// /// If the collection is not in an account's storage, `from` will be `nil`. /// - pub event Withdraw(id: UInt64, from: Address?) + access(all) event Withdrawn(type: String, id: UInt64, uuid: UInt64, from: Address?, providerUUID: UInt64) /// Event that emitted when a token is deposited to a collection. + /// Indicates the type, id, uuid, the owner of the collection that it was deposited to, + /// and the UUID of the collection it was deposited to /// - /// It indicates the owner of the collection that it was deposited to. + /// If the collection is not in an account's storage, `from`, will be `nil`. /// - pub event Deposit(id: UInt64, to: Address?) + access(all) event Deposited(type: String, id: UInt64, uuid: UInt64, to: Address?, collectionUUID: UInt64) - /// Interface that the NFTs have to conform to - /// The metadata views methods are included here temporarily - /// because enforcing the metadata interfaces in the standard - /// would break many contracts in an upgrade. Those breaking changes - /// are being saved for the stable cadence milestone + /// Interface that the NFTs must conform to /// - pub resource interface INFT { - /// The unique ID that each NFT has - pub let id: UInt64 + access(all) resource interface NFT: ViewResolver.Resolver { - /// Function that returns all the Metadata Views implemented by a Non Fungible Token - /// - /// @return An array of Types defining the implemented views. This value will be used by - /// developers to know which parameter to pass to the resolveView() method. - /// - pub fun getViews(): [Type] { - return [] + /// unique ID for the NFT + access(all) let id: UInt64 + + /// Event that is emitted automatically every time a resource is destroyed + /// The type information is included in the metadata event so it is not needed as an argument + access(all) event ResourceDestroyed(id: UInt64 = self.id, uuid: UInt64 = self.uuid) + + /// createEmptyCollection creates an empty Collection that is able to store the NFT + /// and returns it to the caller so that they can own NFTs + /// @return A an empty collection that can store this NFT + access(all) fun createEmptyCollection(): @{Collection} { + post { + result.getLength() == 0: "The created collection must be empty!" + result.isSupportedNFTType(type: self.getType()): "The created collection must support this NFT type" + } } - /// Function that resolves a metadata view for this token. + /// Gets all the NFTs that this NFT directly owns + /// @return A dictionary of all subNFTS keyed by type + access(all) view fun getAvailableSubNFTS(): {Type: UInt64} { + return {} + } + + /// Get a reference to an NFT that this NFT owns + /// Both arguments are optional to allow the NFT to choose + /// how it returns sub NFTs depending on what arguments are provided + /// For example, if `type` has a value, but `id` doesn't, the NFT + /// can choose which NFT of that type to return if there is a "default" + /// If both are `nil`, then NFTs that only store a single NFT can just return + /// that. This helps callers who aren't sure what they are looking for /// - /// @param view: The Type of the desired view. - /// @return A structure representing the requested view. + /// @param type: The Type of the desired NFT + /// @param id: The id of the NFT to borrow /// - pub fun resolveView(_ view: Type): AnyStruct? { + /// @return A structure representing the requested view. + access(all) fun getSubNFT(type: Type, id: UInt64) : &{NonFungibleToken.NFT}? { return nil } } - /// Requirement that all conforming NFT smart contracts have - /// to define a resource called NFT that conforms to INFT + /// Interface to mediate withdrawals from a resource, usually a Collection /// - pub resource NFT: INFT { - pub let id: UInt64 - } + access(all) resource interface Provider { - /// Interface to mediate withdraws from the Collection - /// - pub resource interface Provider { - /// Removes an NFT from the resource implementing it and moves it to the caller - /// - /// @param withdrawID: The ID of the NFT that will be removed - /// @return The NFT resource removed from the implementing resource - /// - pub fun withdraw(withdrawID: UInt64): @NFT { + // We emit withdraw events from the provider interface because conficting withdraw + // events aren't as confusing to event listeners as conflicting deposit events + + /// withdraw removes an NFT from the collection and moves it to the caller + /// It does not specify whether the ID is UUID or not + /// @param withdrawID: The id of the NFT to withdraw from the collection + access(Withdraw) fun withdraw(withdrawID: UInt64): @{NFT} { post { result.id == withdrawID: "The ID of the withdrawn token must be the same as the requested ID" + emit Withdrawn(type: result.getType().identifier, id: result.id, uuid: result.uuid, from: self.owner?.address, providerUUID: self.uuid) } } } /// Interface to mediate deposits to the Collection /// - pub resource interface Receiver { - - /// Adds an NFT to the resource implementing it - /// - /// @param token: The NFT resource that will be deposited - /// - pub fun deposit(token: @NFT) + access(all) resource interface Receiver { + + /// deposit takes an NFT as an argument and adds it to the Collection + /// @param token: The NFT to deposit + access(all) fun deposit(token: @{NFT}) + + /// getSupportedNFTTypes returns a list of NFT types that this receiver accepts + /// @return A dictionary of types mapped to booleans indicating if this + /// reciever supports it + access(all) view fun getSupportedNFTTypes(): {Type: Bool} + + /// Returns whether or not the given type is accepted by the collection + /// A collection that can accept any type should just return true by default + /// @param type: An NFT type + /// @return A boolean indicating if this receiver can recieve the desired NFT type + access(all) view fun isSupportedNFTType(type: Type): Bool } - /// Interface that an account would commonly - /// publish for their collection - /// - pub resource interface CollectionPublic { - pub fun deposit(token: @NFT) - pub fun getIDs(): [UInt64] - pub fun borrowNFT(id: UInt64): &NFT - /// Safe way to borrow a reference to an NFT that does not panic - /// - /// @param id: The ID of the NFT that want to be borrowed - /// @return An optional reference to the desired NFT, will be nil if the passed id does not exist - /// - pub fun borrowNFTSafe(id: UInt64): &NFT? { - post { - result == nil || result!.id == id: "The returned reference's ID does not match the requested ID" - } - return nil - } + /// Kept for backwards-compatibility reasons + access(all) resource interface CollectionPublic { + access(all) fun deposit(token: @{NFT}) + access(all) view fun getLength(): Int + access(all) view fun getIDs(): [UInt64] + access(all) fun forEachID(_ f: fun (UInt64): Bool): Void + access(all) view fun borrowNFT(_ id: UInt64): &{NFT}? } /// Requirement for the concrete resource type /// to be declared in the implementing contract /// - pub resource Collection: Provider, Receiver, CollectionPublic { + access(all) resource interface Collection: Provider, Receiver, CollectionPublic, ViewResolver.ResolverCollection { - /// Dictionary to hold the NFTs in the Collection - pub var ownedNFTs: @{UInt64: NFT} + access(all) var ownedNFTs: @{UInt64: {NonFungibleToken.NFT}} - /// Removes an NFT from the collection and moves it to the caller - /// - /// @param withdrawID: The ID of the NFT that will be withdrawn - /// @return The resource containing the desired NFT - /// - pub fun withdraw(withdrawID: UInt64): @NFT + /// deposit takes a NFT as an argument and stores it in the collection + /// @param token: The NFT to deposit into the collection + access(all) fun deposit(token: @{NonFungibleToken.NFT}) { + pre { + // We emit the deposit event in the `Collection` interface + // because the `Collection` interface is almost always the final destination + // of tokens and deposit emissions from custom receivers could be confusing + // and hard to reconcile to event listeners + emit Deposited(type: token.getType().identifier, id: token.id, uuid: token.uuid, to: self.owner?.address, collectionUUID: self.uuid) + } + } - /// Takes a NFT and adds it to the collections dictionary - /// and adds the ID to the ID array - /// - /// @param token: An NFT resource - /// - pub fun deposit(token: @NFT) + /// Gets the amount of NFTs stored in the collection + /// @return An integer indicating the size of the collection + access(all) view fun getLength(): Int { + return self.ownedNFTs.length + } - /// Returns an array of the IDs that are in the collection - /// - /// @return An array containing all the IDs on the collection - /// - pub fun getIDs(): [UInt64] + /// Allows a given function to iterate through the list + /// of owned NFT IDs in a collection without first + /// having to load the entire list into memory + access(all) fun forEachID(_ f: fun (UInt64): Bool): Void { + self.ownedNFTs.forEachKey(f) + } - /// Returns a borrowed reference to an NFT in the collection - /// so that the caller can read data and call methods from it + /// Borrows a reference to an NFT stored in the collection + /// If the NFT with the specified ID is not in the collection, + /// the function should return `nil` and not panic. /// - /// @param id: The ID of the NFT that want to be borrowed - /// @return A reference to the NFT - /// - pub fun borrowNFT(id: UInt64): &NFT { - pre { - self.ownedNFTs[id] != nil: "NFT does not exist in the collection!" + /// @param id: The desired nft id in the collection to return a referece for. + /// @return An optional reference to the NFT + access(all) view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}? { + post { + (result == nil) || (result?.id == id): + "Cannot borrow NFT reference: The ID of the returned reference does not match the ID that was specified" + } + } + + /// createEmptyCollection creates an empty Collection of the same type + /// and returns it to the caller + /// @return A an empty collection of the same type + access(all) fun createEmptyCollection(): @{Collection} { + post { + result.getType() == self.getType(): "The created collection does not have the same type as this collection" + result.getLength() == 0: "The created collection must be empty!" } } } - /// Creates an empty Collection and returns it to the caller so that they can own NFTs - /// - /// @return A new Collection resource - /// - pub fun createEmptyCollection(): @Collection { + /// createEmptyCollection creates an empty Collection for the specified NFT type + /// and returns it to the caller so that they can own NFTs + /// @param nftType: The desired nft type to return a collection for. + /// @return An array of NFT Types that the implementing contract defines. + access(all) fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection} { post { result.getIDs().length == 0: "The created collection must be empty!" } } -} - \ No newline at end of file +} \ No newline at end of file diff --git a/contracts/TokenForwarding.cdc b/contracts/TokenForwarding.cdc index 82d4e50..e54d734 100644 --- a/contracts/TokenForwarding.cdc +++ b/contracts/TokenForwarding.cdc @@ -16,29 +16,37 @@ their tokens to. import "FungibleToken" -pub contract TokenForwarding { +access(all) contract TokenForwarding { // Event that is emitted when tokens are deposited to the target receiver - pub event ForwardedDeposit(amount: UFix64, from: Address?) + access(all) event ForwardedDeposit(amount: UFix64, from: Address?) - pub resource interface ForwarderPublic { - pub fun check(): Bool - pub fun safeBorrow(): &{FungibleToken.Receiver}? + access(all) resource interface ForwarderPublic { + access(all) fun check(): Bool + access(all) fun safeBorrow(): &{FungibleToken.Receiver}? } - pub resource Forwarder: FungibleToken.Receiver, ForwarderPublic { + access(all) resource Forwarder: FungibleToken.Receiver, ForwarderPublic { // This is where the deposited tokens will be sent. // The type indicates that it is a reference to a receiver // access(self) var recipient: Capability + access(all) view fun getSupportedVaultTypes(): {Type: Bool} { + return {} + } + + access(all) view fun isSupportedVaultType(type: Type): Bool { + return true + } + // deposit // // Function that takes a Vault object as an argument and forwards // it to the recipient's Vault using the stored reference // - pub fun deposit(from: @FungibleToken.Vault) { + access(all) fun deposit(from: @{FungibleToken.Vault}) { let receiverRef = self.recipient.borrow<&{FungibleToken.Receiver}>()! let balance = from.balance @@ -48,17 +56,17 @@ pub contract TokenForwarding { emit ForwardedDeposit(amount: balance, from: self.owner?.address) } - pub fun check(): Bool { + access(all) fun check(): Bool { return self.recipient.check<&{FungibleToken.Receiver}>() } - pub fun safeBorrow(): &{FungibleToken.Receiver}? { + access(all) fun safeBorrow(): &{FungibleToken.Receiver}? { return self.recipient.borrow<&{FungibleToken.Receiver}>() } // changeRecipient changes the recipient of the forwarder to the provided recipient // - pub fun changeRecipient(_ newRecipient: Capability) { + access(all) fun changeRecipient(_ newRecipient: Capability) { pre { newRecipient.borrow<&{FungibleToken.Receiver}>() != nil: "Could not borrow Receiver reference from the Capability" } @@ -75,7 +83,7 @@ pub contract TokenForwarding { // createNewForwarder creates a new Forwarder reference with the provided recipient // - pub fun createNewForwarder(recipient: Capability): @Forwarder { + access(all) fun createNewForwarder(recipient: Capability): @Forwarder { return <-create Forwarder(recipient: recipient) } } diff --git a/contracts/ViewResolver.cdc b/contracts/ViewResolver.cdc index be59741..17c1365 100644 --- a/contracts/ViewResolver.cdc +++ b/contracts/ViewResolver.cdc @@ -3,23 +3,56 @@ // // This will allow you to obtain information about a contract without necessarily knowing anything about it. // All you need is its address and name and you're good to go! -pub contract interface ViewResolver { - /// Function that returns all the Metadata Views implemented by the resolving contract +access(all) contract interface ViewResolver { + + /// Function that returns all the Metadata Views implemented by the resolving contract. + /// Some contracts may have multiple resource types that support metadata views + /// so there there is an optional parameter for specify which resource type the caller + /// is looking for views for. + /// Some contract-level views may be type-agnostic. In that case, the contract + /// should return the same views regardless of what type is passed in. /// + /// @param resourceType: An optional resource type to return views for /// @return An array of Types defining the implemented views. This value will be used by /// developers to know which parameter to pass to the resolveView() method. /// - pub fun getViews(): [Type] { - return [] - } + access(all) view fun getContractViews(resourceType: Type?): [Type] /// Function that resolves a metadata view for this token. + /// Some contracts may have multiple resource types that support metadata views + /// so there there is an optional parameter for specify which resource type the caller + /// is looking for views for. + /// Some contract-level views may be type-agnostic. In that case, the contract + /// should return the same views regardless of what type is passed in. /// + /// @param resourceType: An optional resource type to return views for /// @param view: The Type of the desired view. /// @return A structure representing the requested view. /// - pub fun resolveView(_ view: Type): AnyStruct? { - return nil + access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? + + /// Provides access to a set of metadata views. A struct or + /// resource (e.g. an NFT) can implement this interface to provide access to + /// the views that it supports. + /// + access(all) resource interface Resolver { + + /// Same as getViews above, but on a specific NFT instead of a contract + access(all) view fun getViews(): [Type] + + /// Same as resolveView above, but on a specific NFT instead of a contract + access(all) fun resolveView(_ view: Type): AnyStruct? + } + + /// A group of view resolvers indexed by ID. + /// + access(all) resource interface ResolverCollection { + access(all) view fun borrowViewResolver(id: UInt64): &{Resolver}? { + return nil + } + + access(all) view fun getIDs(): [UInt64] { + return [] + } } } - \ No newline at end of file diff --git a/contracts/capability-cache/CapabilityCache.cdc b/contracts/capability-cache/CapabilityCache.cdc new file mode 100644 index 0000000..2c5e776 --- /dev/null +++ b/contracts/capability-cache/CapabilityCache.cdc @@ -0,0 +1,97 @@ +/* +https://github.com/Flowtyio/capability-cache + +CapabilityCache helps manage capabilities which are issued but are not in public paths. +Rather than looping through all capabilities under a storage path and finding one that +matches the Capability type you want, the cache can be used to retrieve them +*/ +access(all) contract CapabilityCache { + + access(all) let basePathIdentifier: String + + access(all) event CapabilityAdded(owner: Address?, cacheUuid: UInt64, namespace: String, resourceType: Type, capabilityType: Type, capabilityID: UInt64) + access(all) event CapabilityRemoved(owner: Address?, cacheUuid: UInt64, namespace: String, resourceType: Type, capabilityType: Type, capabilityID: UInt64) + + // Add to a namespace + access(all) entitlement Add + + // Remove from a namespace + access(all) entitlement Delete + + // Retrieve a cap from the namespace + access(all) entitlement Get + + // Resource that manages capabilities for a provided namespace. Only one capability is permitted per type. + access(all) resource Cache { + // A dictionary of resourceType -> CapabilityType -> Capability + // For example, one might store a Capability for the @TopShot.NFT resource. + // Note that the resource type is not necessarily the type that the borrowed capability is an instance of. This is because some resource definitions + // might be reused. + access(self) let caps: {Type: {Type: Capability}} + + // who is this capability cache maintained by? e.g. flowty, dapper, find? + access(all) let namespace: String + + // Remove a capability, if it exists, + access(Delete) fun removeCapabilityByType(resourceType: Type, capabilityType: Type): Capability? { + if let ref = &self.caps[resourceType] as auth(Mutate) &{Type: Capability}? { + let cap = ref.remove(key: capabilityType) + if cap != nil { + emit CapabilityRemoved(owner: self.owner?.address, cacheUuid: self.uuid, namespace: self.namespace, resourceType: resourceType, capabilityType: capabilityType, capabilityID: cap!.id) + } + } + + return nil + } + + // Adds a capability to the cache. If there is already an entry for the given type, + // it will be returned + access(Add) fun addCapability(resourceType: Type, cap: Capability): Capability? { + pre { + cap.id != 0: "cannot add a capability with id 0" + } + + let capType = cap.getType() + emit CapabilityAdded(owner: self.owner?.address, cacheUuid: self.uuid, namespace: self.namespace, resourceType: resourceType, capabilityType: capType, capabilityID: cap.id) + if let ref = &self.caps[resourceType] as auth(Mutate) &{Type: Capability}? { + return ref.insert(key: capType, cap) + } + + self.caps[resourceType] = { + capType: cap + } + + return nil + } + + // Retrieve a capability key'd by a given type. + access(Get) fun getCapabilityByType(resourceType: Type, capabilityType: Type): Capability? { + if let tmp = self.caps[resourceType] { + return tmp[capabilityType] + } + + return nil + } + + init(namespace: String) { + self.caps = {} + + self.namespace = namespace + } + } + + // There is no uniform storage path for the Capability Cache. Instead, each platform which issues capabilities + // should manage their own cache, and can generate the storage path to store it in with this helper method + access(all) fun getPathForCache(_ namespace: String): StoragePath { + return StoragePath(identifier: self.basePathIdentifier.concat(namespace)) + ?? panic("invalid namespace value") + } + + access(all) fun createCache(namespace: String): @Cache { + return <- create Cache(namespace: namespace) + } + + init() { + self.basePathIdentifier = "CapabilityCache_".concat(self.account.address.toString()).concat("_") + } +} \ No newline at end of file diff --git a/contracts/dapper/DapperUtilityCoin.cdc b/contracts/dapper/DapperUtilityCoin.cdc index 1ac6f30..2eb615d 100644 --- a/contracts/dapper/DapperUtilityCoin.cdc +++ b/contracts/dapper/DapperUtilityCoin.cdc @@ -1,30 +1,32 @@ import "FungibleToken" +import "MetadataViews" +import "FungibleTokenMetadataViews" -pub contract DapperUtilityCoin: FungibleToken { +access(all) contract DapperUtilityCoin: FungibleToken { // Total supply of DapperUtilityCoins in existence - pub var totalSupply: UFix64 + access(all) var totalSupply: UFix64 // Event that is emitted when the contract is created - pub event TokensInitialized(initialSupply: UFix64) + access(all) event TokensInitialized(initialSupply: UFix64) // Event that is emitted when tokens are withdrawn from a Vault - pub event TokensWithdrawn(amount: UFix64, from: Address?) + access(all) event TokensWithdrawn(amount: UFix64, from: Address?) // Event that is emitted when tokens are deposited to a Vault - pub event TokensDeposited(amount: UFix64, to: Address?) + access(all) event TokensDeposited(amount: UFix64, to: Address?) // Event that is emitted when new tokens are minted - pub event TokensMinted(amount: UFix64) + access(all) event TokensMinted(amount: UFix64) // Event that is emitted when tokens are destroyed - pub event TokensBurned(amount: UFix64) + access(all) event TokensBurned(amount: UFix64) // Event that is emitted when a new minter resource is created - pub event MinterCreated(allowedAmount: UFix64) + access(all) event MinterCreated(allowedAmount: UFix64) // Event that is emitted when a new burner resource is created - pub event BurnerCreated() + access(all) event BurnerCreated() // Vault // @@ -38,10 +40,10 @@ pub contract DapperUtilityCoin: FungibleToken { // out of thin air. A special Minter resource needs to be defined to mint // new tokens. // - pub resource Vault: FungibleToken.Provider, FungibleToken.Receiver, FungibleToken.Balance { + access(all) resource Vault: FungibleToken.Vault { // holds the balance of a users tokens - pub var balance: UFix64 + access(all) var balance: UFix64 // initialize the balance at resource creation time init(balance: UFix64) { @@ -57,7 +59,7 @@ pub contract DapperUtilityCoin: FungibleToken { // created Vault to the context that called so it can be deposited // elsewhere. // - pub fun withdraw(amount: UFix64): @FungibleToken.Vault { + access(FungibleToken.Withdraw) fun withdraw(amount: UFix64): @{FungibleToken.Vault} { self.balance = self.balance - amount emit TokensWithdrawn(amount: amount, from: self.owner?.address) return <-create Vault(balance: amount) @@ -70,7 +72,7 @@ pub contract DapperUtilityCoin: FungibleToken { // It is allowed to destroy the sent Vault because the Vault // was a temporary holder of the tokens. The Vault's balance has // been consumed and therefore can be destroyed. - pub fun deposit(from: @FungibleToken.Vault) { + access(all) fun deposit(from: @{FungibleToken.Vault}) { let vault <- from as! @DapperUtilityCoin.Vault self.balance = self.balance + vault.balance emit TokensDeposited(amount: vault.balance, to: self.owner?.address) @@ -78,9 +80,33 @@ pub contract DapperUtilityCoin: FungibleToken { destroy vault } - destroy() { + access(all) view fun isAvailableToWithdraw(amount: UFix64): Bool { + return self.balance >= amount + } + + access(all) view fun getSupportedVaultTypes(): {Type: Bool} { + return {Type<@Vault>(): true} + } + + access(all) view fun isSupportedVaultType(type: Type): Bool { + return type == Type<@Vault>() + } + + access(contract) fun burnCallback() { DapperUtilityCoin.totalSupply = DapperUtilityCoin.totalSupply - self.balance } + + access(all) fun createEmptyVault(): @{FungibleToken.Vault} { + return <- create Vault(balance: 0.0) + } + + access(all) view fun getViews(): [Type]{ + return DapperUtilityCoin.getContractViews(resourceType: nil) + } + + access(all) fun resolveView(_ view: Type): AnyStruct? { + return DapperUtilityCoin.resolveContractView(resourceType: nil, viewType: view) + } } // createEmptyVault @@ -90,16 +116,16 @@ pub contract DapperUtilityCoin: FungibleToken { // and store the returned Vault in their storage in order to allow their // account to be able to receive deposits of this token type. // - pub fun createEmptyVault(): @FungibleToken.Vault { + access(all) fun createEmptyVault(vaultType: Type): @{FungibleToken.Vault} { return <-create Vault(balance: 0.0) } - pub resource Administrator { + access(all) resource Administrator { // createNewMinter // // Function that creates and returns a new minter resource // - pub fun createNewMinter(allowedAmount: UFix64): @Minter { + access(all) fun createNewMinter(allowedAmount: UFix64): @Minter { emit MinterCreated(allowedAmount: allowedAmount) return <-create Minter(allowedAmount: allowedAmount) } @@ -108,7 +134,7 @@ pub contract DapperUtilityCoin: FungibleToken { // // Function that creates and returns a new burner resource // - pub fun createNewBurner(): @Burner { + access(all) fun createNewBurner(): @Burner { emit BurnerCreated() return <-create Burner() } @@ -118,19 +144,19 @@ pub contract DapperUtilityCoin: FungibleToken { // // Resource object that token admin accounts can hold to mint new tokens. // - pub resource Minter { + access(all) resource Minter { // the amount of tokens that the minter is allowed to mint - pub var allowedAmount: UFix64 + access(all) var allowedAmount: UFix64 // mintTokens // // Function that mints new tokens, adds them to the total supply, // and returns them to the calling context. // - pub fun mintTokens(amount: UFix64): @DapperUtilityCoin.Vault { + access(all) fun mintTokens(amount: UFix64): @DapperUtilityCoin.Vault { pre { - amount > UFix64(0): "Amount minted must be greater than zero" + amount > 0.0: "Amount minted must be greater than zero" amount <= self.allowedAmount: "Amount minted must be less than the allowed amount" } DapperUtilityCoin.totalSupply = DapperUtilityCoin.totalSupply + amount @@ -148,7 +174,7 @@ pub contract DapperUtilityCoin: FungibleToken { // // Resource object that token admin accounts can hold to burn tokens. // - pub resource Burner { + access(all) resource Burner { // burnTokens // @@ -157,7 +183,7 @@ pub contract DapperUtilityCoin: FungibleToken { // Note: the burned tokens are automatically subtracted from the // total supply in the Vault destructor. // - pub fun burnTokens(from: @FungibleToken.Vault) { + access(all) fun burnTokens(from: @{FungibleToken.Vault}) { let vault <- from as! @DapperUtilityCoin.Vault let amount = vault.balance destroy vault @@ -165,6 +191,57 @@ pub contract DapperUtilityCoin: FungibleToken { } } + access(all) view fun getContractViews(resourceType: Type?): [Type] { + return [Type(), + Type(), + Type(), + Type()] + } + + access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? { + switch viewType { + case Type(): + return FungibleTokenMetadataViews.FTView( + ftDisplay: self.resolveContractView(resourceType: nil, viewType: Type()) as! FungibleTokenMetadataViews.FTDisplay?, + ftVaultData: self.resolveContractView(resourceType: nil, viewType: Type()) as! FungibleTokenMetadataViews.FTVaultData? + ) + case Type(): + let media = MetadataViews.Media( + file: MetadataViews.HTTPFile( + url: "https://meetdapper.com/" + ), + mediaType: "image/svg+xml" + ) + let medias = MetadataViews.Medias([media]) + return FungibleTokenMetadataViews.FTDisplay( + name: "Dapper Utility Coin", + symbol: "DUC", + description: "", + externalURL: MetadataViews.ExternalURL("https://meetdapper.com/"), + logos: medias, + socials: { + "twitter": MetadataViews.ExternalURL("https://twitter.com/hellodapper") + } + ) + case Type(): + let vaultRef = DapperUtilityCoin.account.storage.borrow(from: /storage/dapperUtilityCoinVault) + ?? panic("Could not borrow reference to the contract's Vault!") + return FungibleTokenMetadataViews.FTVaultData( + storagePath: /storage/dapperUtilityCoinVault, + receiverPath: /public/exampleTokenReceiver, + metadataPath: /public/dapperUtilityCoinBalance, + receiverLinkedType: Type<&{FungibleToken.Receiver, FungibleToken.Vault}>(), + metadataLinkedType: Type<&{FungibleToken.Balance, FungibleToken.Vault}>(), + createEmptyVaultFunction: (fun (): @{FungibleToken.Vault} { + return <-vaultRef.createEmptyVault() + }) + ) + case Type(): + return FungibleTokenMetadataViews.TotalSupply(totalSupply: DapperUtilityCoin.totalSupply) + } + return nil + } + init() { // we're using a high value as the balance here to make it look like we've got a ton of money, // just in case some contract manually checks that our balance is sufficient to pay for stuff @@ -172,26 +249,16 @@ pub contract DapperUtilityCoin: FungibleToken { let admin <- create Administrator() let minter <- admin.createNewMinter(allowedAmount: self.totalSupply) - self.account.save(<-admin, to: /storage/dapperUtilityCoinAdmin) + self.account.storage.save(<-admin, to: /storage/dapperUtilityCoinAdmin) // mint tokens let tokenVault <- minter.mintTokens(amount: self.totalSupply) - self.account.save(<-tokenVault, to: /storage/dapperUtilityCoinVault) + self.account.storage.save(<-tokenVault, to: /storage/dapperUtilityCoinVault) destroy minter - // Create a public capability to the stored Vault that only exposes - // the balance field through the Balance interface - self.account.link<&DapperUtilityCoin.Vault{FungibleToken.Balance}>( - /public/dapperUtilityCoinBalance, - target: /storage/dapperUtilityCoinVault - ) - - // Create a public capability to the stored Vault that only exposes - // the deposit method through the Receiver interface - self.account.link<&{FungibleToken.Receiver}>( - /public/dapperUtilityCoinReceiver, - target: /storage/dapperUtilityCoinVault - ) + let cap = self.account.capabilities.storage.issue<&DapperUtilityCoin.Vault>(/storage/dapperUtilityCoinVault) + self.account.capabilities.publish(cap, at: /public/dapperUtilityCoinBalance) + self.account.capabilities.publish(cap, at: /public/dapperUtilityCoinReceiver) // Emit an event that shows that the contract was initialized emit TokensInitialized(initialSupply: self.totalSupply) diff --git a/contracts/dapper/FlowUtilityToken.cdc b/contracts/dapper/FlowUtilityToken.cdc index 3fedfeb..f137cee 100644 --- a/contracts/dapper/FlowUtilityToken.cdc +++ b/contracts/dapper/FlowUtilityToken.cdc @@ -1,30 +1,32 @@ import "FungibleToken" +import "MetadataViews" +import "FungibleTokenMetadataViews" -pub contract FlowUtilityToken: FungibleToken { +access(all) contract FlowUtilityToken: FungibleToken { - // Total supply of DapperUtilityCoins in existence - pub var totalSupply: UFix64 + // Total supply of FlowUtilityTokens in existence + access(all) var totalSupply: UFix64 // Event that is emitted when the contract is created - pub event TokensInitialized(initialSupply: UFix64) + access(all) event TokensInitialized(initialSupply: UFix64) // Event that is emitted when tokens are withdrawn from a Vault - pub event TokensWithdrawn(amount: UFix64, from: Address?) + access(all) event TokensWithdrawn(amount: UFix64, from: Address?) // Event that is emitted when tokens are deposited to a Vault - pub event TokensDeposited(amount: UFix64, to: Address?) + access(all) event TokensDeposited(amount: UFix64, to: Address?) // Event that is emitted when new tokens are minted - pub event TokensMinted(amount: UFix64) + access(all) event TokensMinted(amount: UFix64) // Event that is emitted when tokens are destroyed - pub event TokensBurned(amount: UFix64) + access(all) event TokensBurned(amount: UFix64) // Event that is emitted when a new minter resource is created - pub event MinterCreated(allowedAmount: UFix64) + access(all) event MinterCreated(allowedAmount: UFix64) // Event that is emitted when a new burner resource is created - pub event BurnerCreated() + access(all) event BurnerCreated() // Vault // @@ -38,10 +40,10 @@ pub contract FlowUtilityToken: FungibleToken { // out of thin air. A special Minter resource needs to be defined to mint // new tokens. // - pub resource Vault: FungibleToken.Provider, FungibleToken.Receiver, FungibleToken.Balance { + access(all) resource Vault: FungibleToken.Vault { // holds the balance of a users tokens - pub var balance: UFix64 + access(all) var balance: UFix64 // initialize the balance at resource creation time init(balance: UFix64) { @@ -57,7 +59,7 @@ pub contract FlowUtilityToken: FungibleToken { // created Vault to the context that called so it can be deposited // elsewhere. // - pub fun withdraw(amount: UFix64): @FungibleToken.Vault { + access(FungibleToken.Withdraw) fun withdraw(amount: UFix64): @{FungibleToken.Vault} { self.balance = self.balance - amount emit TokensWithdrawn(amount: amount, from: self.owner?.address) return <-create Vault(balance: amount) @@ -70,17 +72,41 @@ pub contract FlowUtilityToken: FungibleToken { // It is allowed to destroy the sent Vault because the Vault // was a temporary holder of the tokens. The Vault's balance has // been consumed and therefore can be destroyed. - pub fun deposit(from: @FungibleToken.Vault) { + access(all) fun deposit(from: @{FungibleToken.Vault}) { let vault <- from as! @FlowUtilityToken.Vault self.balance = self.balance + vault.balance emit TokensDeposited(amount: vault.balance, to: self.owner?.address) vault.balance = 0.0 destroy vault } + + access(all) view fun isAvailableToWithdraw(amount: UFix64): Bool { + return self.balance >= amount + } + + access(all) view fun getSupportedVaultTypes(): {Type: Bool} { + return {Type<@Vault>(): true} + } + + access(all) view fun isSupportedVaultType(type: Type): Bool { + return type == Type<@Vault>() + } - destroy() { + access(contract) fun burnCallback() { FlowUtilityToken.totalSupply = FlowUtilityToken.totalSupply - self.balance } + + access(all) fun createEmptyVault(): @{FungibleToken.Vault} { + return <- create Vault(balance: 0.0) + } + + access(all) view fun getViews(): [Type]{ + return FlowUtilityToken.getContractViews(resourceType: nil) + } + + access(all) fun resolveView(_ view: Type): AnyStruct? { + return FlowUtilityToken.resolveContractView(resourceType: nil, viewType: view) + } } // createEmptyVault @@ -90,16 +116,16 @@ pub contract FlowUtilityToken: FungibleToken { // and store the returned Vault in their storage in order to allow their // account to be able to receive deposits of this token type. // - pub fun createEmptyVault(): @FungibleToken.Vault { + access(all) fun createEmptyVault(vaultType: Type): @{FungibleToken.Vault} { return <-create Vault(balance: 0.0) } - pub resource Administrator { + access(all) resource Administrator { // createNewMinter // // Function that creates and returns a new minter resource // - pub fun createNewMinter(allowedAmount: UFix64): @Minter { + access(all) fun createNewMinter(allowedAmount: UFix64): @Minter { emit MinterCreated(allowedAmount: allowedAmount) return <-create Minter(allowedAmount: allowedAmount) } @@ -108,7 +134,7 @@ pub contract FlowUtilityToken: FungibleToken { // // Function that creates and returns a new burner resource // - pub fun createNewBurner(): @Burner { + access(all) fun createNewBurner(): @Burner { emit BurnerCreated() return <-create Burner() } @@ -118,19 +144,19 @@ pub contract FlowUtilityToken: FungibleToken { // // Resource object that token admin accounts can hold to mint new tokens. // - pub resource Minter { + access(all) resource Minter { // the amount of tokens that the minter is allowed to mint - pub var allowedAmount: UFix64 + access(all) var allowedAmount: UFix64 // mintTokens // // Function that mints new tokens, adds them to the total supply, // and returns them to the calling context. // - pub fun mintTokens(amount: UFix64): @FlowUtilityToken.Vault { + access(all) fun mintTokens(amount: UFix64): @FlowUtilityToken.Vault { pre { - amount > UFix64(0): "Amount minted must be greater than zero" + amount > 0.0: "Amount minted must be greater than zero" amount <= self.allowedAmount: "Amount minted must be less than the allowed amount" } FlowUtilityToken.totalSupply = FlowUtilityToken.totalSupply + amount @@ -148,7 +174,7 @@ pub contract FlowUtilityToken: FungibleToken { // // Resource object that token admin accounts can hold to burn tokens. // - pub resource Burner { + access(all) resource Burner { // burnTokens // @@ -157,7 +183,7 @@ pub contract FlowUtilityToken: FungibleToken { // Note: the burned tokens are automatically subtracted from the // total supply in the Vault destructor. // - pub fun burnTokens(from: @FungibleToken.Vault) { + access(all) fun burnTokens(from: @{FungibleToken.Vault}) { let vault <- from as! @FlowUtilityToken.Vault let amount = vault.balance destroy vault @@ -165,6 +191,57 @@ pub contract FlowUtilityToken: FungibleToken { } } + access(all) view fun getContractViews(resourceType: Type?): [Type] { + return [Type(), + Type(), + Type(), + Type()] + } + + access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? { + switch viewType { + case Type(): + return FungibleTokenMetadataViews.FTView( + ftDisplay: self.resolveContractView(resourceType: nil, viewType: Type()) as! FungibleTokenMetadataViews.FTDisplay?, + ftVaultData: self.resolveContractView(resourceType: nil, viewType: Type()) as! FungibleTokenMetadataViews.FTVaultData? + ) + case Type(): + let media = MetadataViews.Media( + file: MetadataViews.HTTPFile( + url: "https://meetdapper.com/" + ), + mediaType: "image/svg+xml" + ) + let medias = MetadataViews.Medias([media]) + return FungibleTokenMetadataViews.FTDisplay( + name: "Dapper Utility Coin", + symbol: "DUC", + description: "", + externalURL: MetadataViews.ExternalURL("https://meetdapper.com/"), + logos: medias, + socials: { + "twitter": MetadataViews.ExternalURL("https://twitter.com/hellodapper") + } + ) + case Type(): + let vaultRef = FlowUtilityToken.account.storage.borrow(from: /storage/flowUtilityTokenVault) + ?? panic("Could not borrow reference to the contract's Vault!") + return FungibleTokenMetadataViews.FTVaultData( + storagePath: /storage/flowUtilityTokenVault, + receiverPath: /public/exampleTokenReceiver, + metadataPath: /public/flowUtilityTokenBalance, + receiverLinkedType: Type<&{FungibleToken.Receiver, FungibleToken.Vault}>(), + metadataLinkedType: Type<&{FungibleToken.Balance, FungibleToken.Vault}>(), + createEmptyVaultFunction: (fun (): @{FungibleToken.Vault} { + return <-vaultRef.createEmptyVault() + }) + ) + case Type(): + return FungibleTokenMetadataViews.TotalSupply(totalSupply: FlowUtilityToken.totalSupply) + } + return nil + } + init() { // we're using a high value as the balance here to make it look like we've got a ton of money, // just in case some contract manually checks that our balance is sufficient to pay for stuff @@ -172,26 +249,16 @@ pub contract FlowUtilityToken: FungibleToken { let admin <- create Administrator() let minter <- admin.createNewMinter(allowedAmount: self.totalSupply) - self.account.save(<-admin, to: /storage/flowUtilityTokenAdmin) + self.account.storage.save(<-admin, to: /storage/flowUtilityTokenAdmin) // mint tokens let tokenVault <- minter.mintTokens(amount: self.totalSupply) - self.account.save(<-tokenVault, to: /storage/flowUtilityTokenVault) + self.account.storage.save(<-tokenVault, to: /storage/flowUtilityTokenVault) destroy minter - // Create a public capability to the stored Vault that only exposes - // the balance field through the Balance interface - self.account.link<&FlowUtilityToken.Vault{FungibleToken.Balance}>( - /public/flowUtilityTokenBalance, - target: /storage/flowUtilityTokenVault - ) - - // Create a public capability to the stored Vault that only exposes - // the deposit method through the Receiver interface - self.account.link<&{FungibleToken.Receiver}>( - /public/flowUtilityTokenReceiver, - target: /storage/flowUtilityTokenVault - ) + let cap = self.account.capabilities.storage.issue<&FlowUtilityToken.Vault>(/storage/flowUtilityTokenVault) + self.account.capabilities.publish(cap, at: /public/flowUtilityTokenBalance) + self.account.capabilities.publish(cap, at: /public/flowUtilityTokenReceiver) // Emit an event that shows that the contract was initialized emit TokensInitialized(initialSupply: self.totalSupply) diff --git a/contracts/dapper/TopShot.cdc b/contracts/dapper/TopShot.cdc index 696aebe..50b5e5f 100644 --- a/contracts/dapper/TopShot.cdc +++ b/contracts/dapper/TopShot.cdc @@ -47,62 +47,63 @@ import "FungibleToken" import "NonFungibleToken" import "MetadataViews" import "TopShotLocking" +import "ViewResolver" -pub contract TopShot: NonFungibleToken { +access(all) contract TopShot: NonFungibleToken { // ----------------------------------------------------------------------- // TopShot deployment variables // ----------------------------------------------------------------------- // The network the contract is deployed on - pub fun Network() : String { return "emulator" } + access(all) view fun Network() : String { return "emulator" } // The address to which royalties should be deposited - pub fun RoyaltyAddress() : Address { return TopShot.account.address } + access(all) view fun RoyaltyAddress() : Address { return TopShot.account.address } // The path to the Subedition Admin resource belonging to the Account // which the contract is deployed on - pub fun SubeditionAdminStoragePath() : StoragePath { return /storage/TopShotSubeditionAdmin} + access(all) view fun SubeditionAdminStoragePath() : StoragePath { return /storage/TopShotSubeditionAdmin} // ----------------------------------------------------------------------- // TopShot contract Events // ----------------------------------------------------------------------- // Emitted when the TopShot contract is created - pub event ContractInitialized() + access(all) event ContractInitialized() // Emitted when a new Play struct is created - pub event PlayCreated(id: UInt32, metadata: {String:String}) + access(all) event PlayCreated(id: UInt32, metadata: {String:String}) // Emitted when a new series has been triggered by an admin - pub event NewSeriesStarted(newCurrentSeries: UInt32) + access(all) event NewSeriesStarted(newCurrentSeries: UInt32) // Events for Set-Related actions // // Emitted when a new Set is created - pub event SetCreated(setID: UInt32, series: UInt32) + access(all) event SetCreated(setID: UInt32, series: UInt32) // Emitted when a new Play is added to a Set - pub event PlayAddedToSet(setID: UInt32, playID: UInt32) + access(all) event PlayAddedToSet(setID: UInt32, playID: UInt32) // Emitted when a Play is retired from a Set and cannot be used to mint - pub event PlayRetiredFromSet(setID: UInt32, playID: UInt32, numMoments: UInt32) + access(all) event PlayRetiredFromSet(setID: UInt32, playID: UInt32, numMoments: UInt32) // Emitted when a Set is locked, meaning Plays cannot be added - pub event SetLocked(setID: UInt32) + access(all) event SetLocked(setID: UInt32) // Emitted when a Moment is minted from a Set - pub event MomentMinted(momentID: UInt64, playID: UInt32, setID: UInt32, serialNumber: UInt32, subeditionID: UInt32) + access(all) event MomentMinted(momentID: UInt64, playID: UInt32, setID: UInt32, serialNumber: UInt32, subeditionID: UInt32) // Events for Collection-related actions // // Emitted when a moment is withdrawn from a Collection - pub event Withdraw(id: UInt64, from: Address?) + access(all) event Withdraw(id: UInt64, from: Address?) // Emitted when a moment is deposited into a Collection - pub event Deposit(id: UInt64, to: Address?) + access(all) event Deposit(id: UInt64, to: Address?) // Emitted when a Moment is destroyed - pub event MomentDestroyed(id: UInt64) + access(all) event MomentDestroyed(id: UInt64) // Emitted when a Subedition is created - pub event SubeditionCreated(subeditionID: UInt32, name: String, metadata: {String:String}) + access(all) event SubeditionCreated(subeditionID: UInt32, name: String, metadata: {String:String}) // Emitted when a Subedition is linked to the specific Moment - pub event SubeditionAddedToMoment(momentID: UInt64, subeditionID: UInt32, setID: UInt32, playID: UInt32) + access(all) event SubeditionAddedToMoment(momentID: UInt64, subeditionID: UInt32, setID: UInt32, playID: UInt32) // ----------------------------------------------------------------------- // TopShot contract-level fields. @@ -112,7 +113,7 @@ pub contract TopShot: NonFungibleToken { // Series that this Set belongs to. // Series is a concept that indicates a group of Sets through time. // Many Sets can exist at a time, but only one series. - pub var currentSeries: UInt32 + access(all) var currentSeries: UInt32 // Variable size dictionary of Play structs access(self) var playDatas: {UInt32: Play} @@ -126,17 +127,17 @@ pub contract TopShot: NonFungibleToken { // The ID that is used to create Plays. // Every time a Play is created, playID is assigned // to the new Play's ID and then is incremented by 1. - pub var nextPlayID: UInt32 + access(all) var nextPlayID: UInt32 // The ID that is used to create Sets. Every time a Set is created // setID is assigned to the new set's ID and then is incremented by 1. - pub var nextSetID: UInt32 + access(all) var nextSetID: UInt32 // The total number of Top shot Moment NFTs that have been created // Because NFTs can be destroyed, it doesn't necessarily mean that this // reflects the total number of NFTs in existence, just the number that // have been minted to date. Also used as global moment IDs for minting. - pub var totalSupply: UInt64 + access(all) var totalSupply: UInt64 // ----------------------------------------------------------------------- // TopShot contract-level Composite Type definitions @@ -156,16 +157,16 @@ pub contract TopShot: NonFungibleToken { // its metadata. The plays are publicly accessible, so anyone can // read the metadata associated with a specific play ID // - pub struct Play { + access(all) struct Play { // The unique ID for the Play - pub let playID: UInt32 + access(all) let playID: UInt32 // Stores all the metadata about the play as a string mapping // This is not the long term way NFT metadata will be stored. It's a temporary // construct while we figure out a better way to do metadata. // - pub let metadata: {String: String} + access(all) let metadata: {String: String} init(metadata: {String: String}) { pre { @@ -196,19 +197,19 @@ pub contract TopShot: NonFungibleToken { // at the end of the contract. Only the admin has the ability // to modify any data in the private Set resource. // - pub struct SetData { + access(all) struct SetData { // Unique ID for the Set - pub let setID: UInt32 + access(all) let setID: UInt32 // Name of the Set // ex. "Times when the Toronto Raptors choked in the playoffs" - pub let name: String + access(all) let name: String // Series that this Set belongs to. // Series is a concept that indicates a group of Sets through time. // Many Sets can exist at a time, but only one series. - pub let series: UInt32 + access(all) let series: UInt32 init(name: String) { pre { @@ -239,10 +240,10 @@ pub contract TopShot: NonFungibleToken { // // If retireAll() and lock() are called back-to-back, // the Set is closed off forever and nothing more can be done with it. - pub resource Set { + access(all) resource Set { // Unique ID for the set - pub let setID: UInt32 + access(all) let setID: UInt32 // Array of plays that are a part of this set. // When a play is added to the set, its ID gets appended here. @@ -263,7 +264,7 @@ pub contract TopShot: NonFungibleToken { // If a Set is locked, Plays cannot be added, but // Moments can still be minted from Plays // that exist in the Set. - pub var locked: Bool + access(all) var locked: Bool // Mapping of Play IDs that indicates the number of Moments // that have been minted for specific Plays in this Set. @@ -291,7 +292,7 @@ pub contract TopShot: NonFungibleToken { // The Set needs to be not locked // The Play can't have already been added to the Set // - pub fun addPlay(playID: UInt32) { + access(all) fun addPlay(playID: UInt32) { pre { TopShot.playDatas[playID] != nil: "Cannot add the Play to Set: Play doesn't exist." !self.locked: "Cannot add the play to the Set after the set has been locked." @@ -315,7 +316,7 @@ pub contract TopShot: NonFungibleToken { // Parameters: playIDs: The IDs of the Plays that are being added // as an array // - pub fun addPlays(playIDs: [UInt32]) { + access(all) fun addPlays(playIDs: [UInt32]) { for play in playIDs { self.addPlay(playID: play) } @@ -328,7 +329,7 @@ pub contract TopShot: NonFungibleToken { // Pre-Conditions: // The Play is part of the Set and not retired (available for minting). // - pub fun retirePlay(playID: UInt32) { + access(all) fun retirePlay(playID: UInt32) { pre { self.retired[playID] != nil: "Cannot retire the Play: Play doesn't exist in this set!" } @@ -343,7 +344,7 @@ pub contract TopShot: NonFungibleToken { // retireAll retires all the plays in the Set // Afterwards, none of the retired Plays will be able to mint new Moments // - pub fun retireAll() { + access(all) fun retireAll() { for play in self.plays { self.retirePlay(playID: play) } @@ -353,7 +354,7 @@ pub contract TopShot: NonFungibleToken { // // Pre-Conditions: // The Set should not be locked - pub fun lock() { + access(all) fun lock() { if !self.locked { self.locked = true emit SetLocked(setID: self.setID) @@ -369,7 +370,7 @@ pub contract TopShot: NonFungibleToken { // // Returns: The NFT that was minted // - pub fun mintMoment(playID: UInt32): @NFT { + access(all) fun mintMoment(playID: UInt32): @NFT { pre { self.retired[playID] != nil: "Cannot mint the moment: This play doesn't exist." !self.retired[playID]!: "Cannot mint the moment from this play: This play has been retired." @@ -399,7 +400,7 @@ pub contract TopShot: NonFungibleToken { // // Returns: Collection object that contains all the Moments that were minted // - pub fun batchMintMoment(playID: UInt32, quantity: UInt64): @Collection { + access(all) fun batchMintMoment(playID: UInt32, quantity: UInt64): @Collection { let newCollection <- create Collection() var i: UInt64 = 0 @@ -421,7 +422,7 @@ pub contract TopShot: NonFungibleToken { // // Returns: The NFT that was minted // - pub fun mintMomentWithSubedition(playID: UInt32, subeditionID: UInt32): @NFT { + access(all) fun mintMomentWithSubedition(playID: UInt32, subeditionID: UInt32): @NFT { pre { self.retired[playID] != nil: "Cannot mint the moment: This play doesn't exist." !self.retired[playID]!: "Cannot mint the moment from this play: This play has been retired." @@ -429,7 +430,7 @@ pub contract TopShot: NonFungibleToken { // Gets the number of Moments that have been minted for this subedition // to use as this Moment's serial number - let subeditionRef = TopShot.account.borrow<&SubeditionAdmin>(from: TopShot.SubeditionAdminStoragePath()) + let subeditionRef = TopShot.account.storage.borrow<&SubeditionAdmin>(from: TopShot.SubeditionAdminStoragePath()) ?? panic("No subedition admin resource in storage") let numInSubedition = subeditionRef.getNumberMintedPerSubedition(setID: self.setID, @@ -463,7 +464,7 @@ pub contract TopShot: NonFungibleToken { // // Returns: Collection object that contains all the Moments that were minted // - pub fun batchMintMomentWithSubedition(playID: UInt32, quantity: UInt64, subeditionID: UInt32): @Collection { + access(all) fun batchMintMomentWithSubedition(playID: UInt32, quantity: UInt64, subeditionID: UInt32): @Collection { let newCollection <- create Collection() var i: UInt64 = 0 @@ -476,15 +477,15 @@ pub contract TopShot: NonFungibleToken { return <-newCollection } - pub fun getPlays(): [UInt32] { + access(all) view fun getPlays(): [UInt32] { return self.plays } - pub fun getRetired(): {UInt32: Bool} { + access(all) view fun getRetired(): {UInt32: Bool} { return self.retired } - pub fun getNumMintedPerPlay(): {UInt32: UInt32} { + access(all) view fun getNumMintedPerPlay(): {UInt32: UInt32} { return self.numberMintedPerPlay } } @@ -494,13 +495,13 @@ pub contract TopShot: NonFungibleToken { // with the desired set ID // let setData = TopShot.QuerySetData(setID: 12) // - pub struct QuerySetData { - pub let setID: UInt32 - pub let name: String - pub let series: UInt32 + access(all) struct QuerySetData { + access(all) let setID: UInt32 + access(all) let name: String + access(all) let series: UInt32 access(self) var plays: [UInt32] access(self) var retired: {UInt32: Bool} - pub var locked: Bool + access(all) var locked: Bool access(self) var numberMintedPerPlay: {UInt32: UInt32} init(setID: UInt32) { @@ -514,36 +515,36 @@ pub contract TopShot: NonFungibleToken { self.setID = setID self.name = setData.name self.series = setData.series - self.plays = set.plays - self.retired = set.retired + self.plays = set.getPlays() + self.retired = set.getRetired() self.locked = set.locked - self.numberMintedPerPlay = set.numberMintedPerPlay + self.numberMintedPerPlay = set.getNumMintedPerPlay() } - pub fun getPlays(): [UInt32] { + access(all) view fun getPlays(): [UInt32] { return self.plays } - pub fun getRetired(): {UInt32: Bool} { + access(all) view fun getRetired(): {UInt32: Bool} { return self.retired } - pub fun getNumberMintedPerPlay(): {UInt32: UInt32} { + access(all) view fun getNumberMintedPerPlay(): {UInt32: UInt32} { return self.numberMintedPerPlay } } - pub struct MomentData { + access(all) struct MomentData { // The ID of the Set that the Moment comes from - pub let setID: UInt32 + access(all) let setID: UInt32 // The ID of the Play that the Moment references - pub let playID: UInt32 + access(all) let playID: UInt32 // The place in the edition that this Moment was minted // Otherwise know as the serial number - pub let serialNumber: UInt32 + access(all) let serialNumber: UInt32 init(setID: UInt32, playID: UInt32, serialNumber: UInt32) { self.setID = setID @@ -556,38 +557,38 @@ pub contract TopShot: NonFungibleToken { // This is an implementation of a custom metadata view for Top Shot. // This view contains the play metadata. // - pub struct TopShotMomentMetadataView { - - pub let fullName: String? - pub let firstName: String? - pub let lastName: String? - pub let birthdate: String? - pub let birthplace: String? - pub let jerseyNumber: String? - pub let draftTeam: String? - pub let draftYear: String? - pub let draftSelection: String? - pub let draftRound: String? - pub let teamAtMomentNBAID: String? - pub let teamAtMoment: String? - pub let primaryPosition: String? - pub let height: String? - pub let weight: String? - pub let totalYearsExperience: String? - pub let nbaSeason: String? - pub let dateOfMoment: String? - pub let playCategory: String? - pub let playType: String? - pub let homeTeamName: String? - pub let awayTeamName: String? - pub let homeTeamScore: String? - pub let awayTeamScore: String? - pub let seriesNumber: UInt32? - pub let setName: String? - pub let serialNumber: UInt32 - pub let playID: UInt32 - pub let setID: UInt32 - pub let numMomentsInEdition: UInt32? + access(all) struct TopShotMomentMetadataView { + + access(all) let fullName: String? + access(all) let firstName: String? + access(all) let lastName: String? + access(all) let birthdate: String? + access(all) let birthplace: String? + access(all) let jerseyNumber: String? + access(all) let draftTeam: String? + access(all) let draftYear: String? + access(all) let draftSelection: String? + access(all) let draftRound: String? + access(all) let teamAtMomentNBAID: String? + access(all) let teamAtMoment: String? + access(all) let primaryPosition: String? + access(all) let height: String? + access(all) let weight: String? + access(all) let totalYearsExperience: String? + access(all) let nbaSeason: String? + access(all) let dateOfMoment: String? + access(all) let playCategory: String? + access(all) let playType: String? + access(all) let homeTeamName: String? + access(all) let awayTeamName: String? + access(all) let homeTeamScore: String? + access(all) let awayTeamScore: String? + access(all) let seriesNumber: UInt32? + access(all) let setName: String? + access(all) let serialNumber: UInt32 + access(all) let playID: UInt32 + access(all) let setID: UInt32 + access(all) let numMomentsInEdition: UInt32? init( fullName: String?, @@ -656,13 +657,13 @@ pub contract TopShot: NonFungibleToken { // The resource that represents the Moment NFTs // - pub resource NFT: NonFungibleToken.INFT, MetadataViews.Resolver { + access(all) resource NFT: NonFungibleToken.NFT { // Global unique moment ID - pub let id: UInt64 + access(all) let id: UInt64 // Struct of Moment metadata - pub let data: MomentData + access(all) let data: MomentData init(serialNumber: UInt32, playID: UInt32, setID: UInt32, subeditionID: UInt32) { // Increment the global Moment IDs @@ -681,12 +682,15 @@ pub contract TopShot: NonFungibleToken { } // If the Moment is destroyed, emit an event to indicate - // to outside ovbservers that it has been destroyed - destroy() { - emit MomentDestroyed(id: self.id) - } - - pub fun name(): String { + // to outside observers that it has been destroyed + access(all) event ResourceDestroyed( + id: UInt64 = self.id, + serialNumber: UInt32 = self.data.serialNumber, + playID: UInt32 = self.data.playID, + setID: UInt32 = self.data.setID + ) + + access(all) view fun name(): String { let fullName: String = TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "FullName") ?? "" let playType: String = TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "PlayType") ?? "" return fullName @@ -708,14 +712,14 @@ pub contract TopShot: NonFungibleToken { /// The description of the Moment. If Tagline property of the play is empty, compose it using the buildDescString function /// If the Tagline property is not empty, use that as the description - pub fun description(): String { + access(all) fun description(): String { let playDesc: String = TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "Tagline") ?? "" return playDesc.length > 0 ? playDesc : self.buildDescString() } // All supported metadata views for the Moment including the Core NFT Views - pub fun getViews(): [Type] { + access(all) view fun getViews(): [Type] { return [ Type(), Type(), @@ -732,7 +736,7 @@ pub contract TopShot: NonFungibleToken { - pub fun resolveView(_ view: Type): AnyStruct? { + access(all) fun resolveView(_ view: Type): AnyStruct? { switch view { case Type(): return MetadataViews.Display( @@ -787,56 +791,13 @@ pub contract TopShot: NonFungibleToken { UInt64(self.data.serialNumber) ) case Type(): - let royaltyReceiver: Capability<&{FungibleToken.Receiver}> = - getAccount(TopShot.RoyaltyAddress()).getCapability<&AnyResource{FungibleToken.Receiver}>(MetadataViews.getRoyaltyReceiverPublicPath()) - return MetadataViews.Royalties( - royalties: [ - MetadataViews.Royalty( - receiver: royaltyReceiver, - cut: 0.05, - description: "NBATopShot marketplace royalty" - ) - ] - ) + return TopShot.resolveContractView(resourceType: nil, viewType: Type()) case Type(): return MetadataViews.ExternalURL(self.getMomentURL()) case Type(): - return MetadataViews.NFTCollectionData( - storagePath: /storage/MomentCollection, - publicPath: /public/MomentCollection, - providerPath: /private/MomentCollection, - publicCollection: Type<&TopShot.Collection{TopShot.MomentCollectionPublic}>(), - publicLinkedType: Type<&TopShot.Collection{TopShot.MomentCollectionPublic,NonFungibleToken.Receiver,NonFungibleToken.CollectionPublic,MetadataViews.ResolverCollection}>(), - providerLinkedType: Type<&TopShot.Collection{NonFungibleToken.Provider,TopShot.MomentCollectionPublic,NonFungibleToken.Receiver,NonFungibleToken.CollectionPublic,MetadataViews.ResolverCollection}>(), - createEmptyCollectionFunction: (fun (): @NonFungibleToken.Collection { - return <-TopShot.createEmptyCollection() - }) - ) + return TopShot.resolveContractView(resourceType: nil, viewType: Type()) case Type(): - let bannerImage = MetadataViews.Media( - file: MetadataViews.HTTPFile( - url: "https://nbatopshot.com/static/img/top-shot-logo-horizontal-white.svg" - ), - mediaType: "image/svg+xml" - ) - let squareImage = MetadataViews.Media( - file: MetadataViews.HTTPFile( - url: "https://nbatopshot.com/static/img/og/og.png" - ), - mediaType: "image/png" - ) - return MetadataViews.NFTCollectionDisplay( - name: "NBA-Top-Shot", - description: "NBA Top Shot is your chance to own, sell, and trade official digital collectibles of the NBA and WNBA's greatest plays and players", - externalURL: MetadataViews.ExternalURL("https://nbatopshot.com"), - squareImage: squareImage, - bannerImage: bannerImage, - socials: { - "twitter": MetadataViews.ExternalURL("https://twitter.com/nbatopshot"), - "discord": MetadataViews.ExternalURL("https://discord.com/invite/nbatopshot"), - "instagram": MetadataViews.ExternalURL("https://www.instagram.com/nbatopshot") - } - ) + return TopShot.resolveContractView(resourceType: nil, viewType: Type()) case Type(): // sports radar team id let excludedNames: [String] = ["TeamAtMomentNBAID"] @@ -844,14 +805,15 @@ pub contract TopShot: NonFungibleToken { let traitDictionary: {String: AnyStruct} = { "SeriesNumber": TopShot.getSetSeries(setID: self.data.setID), "SetName": TopShot.getSetName(setID: self.data.setID), - "SerialNumber": self.data.serialNumber + "SerialNumber": self.data.serialNumber, + "Locked": TopShotLocking.isLocked(nftRef: &self as &{NonFungibleToken.NFT}) } // add play specific data let fullDictionary = self.mapPlayData(dict: traitDictionary) return MetadataViews.dictToTraits(dict: fullDictionary, excludedNames: excludedNames) case Type(): return MetadataViews.Medias( - items: [ + [ MetadataViews.Media( file: MetadataViews.HTTPFile( url: self.mediumimage() @@ -875,7 +837,7 @@ pub contract TopShot: NonFungibleToken { // mapPlayData helps build our trait map from play metadata // Returns: The trait map with all non-empty fields from play data added - pub fun mapPlayData(dict: {String: AnyStruct}) : {String: AnyStruct} { + access(all) fun mapPlayData(dict: {String: AnyStruct}) : {String: AnyStruct} { let playMetadata = TopShot.getPlayMetaData(playID: self.data.playID) ?? {} for name in playMetadata.keys { let value = playMetadata[name] ?? "" @@ -888,53 +850,58 @@ pub contract TopShot: NonFungibleToken { // getMomentURL // Returns: The computed external url of the moment - pub fun getMomentURL(): String { + access(all) view fun getMomentURL(): String { return "https://nbatopshot.com/moment/".concat(self.id.toString()) } // getEditionName Moment's edition name is a combination of the Moment's setName and playID // `setName: #playID` - pub fun getEditionName() : String { + access(all) view fun getEditionName() : String { let setName: String = TopShot.getSetName(setID: self.data.setID) ?? "" let editionName = setName.concat(": #").concat(self.data.playID.toString()) return editionName } - pub fun assetPath(): String { + access(all) view fun assetPath(): String { return "https://assets.nbatopshot.com/media/".concat(self.id.toString()) } // returns a url to display an medium sized image - pub fun mediumimage(): String { + access(all) fun mediumimage(): String { let url = self.assetPath().concat("?width=512") return self.appendOptionalParams(url: url, firstDelim: "&") } // a url to display a thumbnail associated with the moment - pub fun thumbnail(): String { + access(all) fun thumbnail(): String { let url = self.assetPath().concat("?width=256") return self.appendOptionalParams(url: url, firstDelim: "&") } // a url to display a video associated with the moment - pub fun video(): String { + access(all) fun video(): String { let url = self.assetPath().concat("/video") return self.appendOptionalParams(url: url, firstDelim: "?") } // appends and optional network param needed to resolve the media - pub fun appendOptionalParams(url: String, firstDelim: String): String { + access(all) fun appendOptionalParams(url: String, firstDelim: String): String { if (TopShot.Network() == "testnet") { return url.concat(firstDelim).concat("testnet") } return url } + + // Create an empty Collection for Pinnacle NFTs and return it to the caller + access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} { + return <- TopShot.createEmptyCollection(nftType: Type<@TopShot.NFT>()) + } } // Admin is a special authorization resource that // allows the owner to perform important functions to modify the // various aspects of the Plays, Sets, and Moments // - pub resource Admin { + access(all) resource Admin { // createPlay creates a new Play struct // and stores it in the Plays dictionary in the TopShot smart contract @@ -945,7 +912,7 @@ pub contract TopShot: NonFungibleToken { // // Returns: the ID of the new Play object // - pub fun createPlay(metadata: {String: String}): UInt32 { + access(all) fun createPlay(metadata: {String: String}): UInt32 { // Create the new Play var newPlay = Play(metadata: metadata) let newID = newPlay.playID @@ -965,7 +932,7 @@ pub contract TopShot: NonFungibleToken { /// Parameters: playID: The ID of the play to update /// tagline: A string to be used as the tagline for the play /// Returns: The ID of the play - pub fun updatePlayTagline(playID: UInt32, tagline: String): UInt32 { + access(all) fun updatePlayTagline(playID: UInt32, tagline: String): UInt32 { let tmpPlay = TopShot.playDatas[playID] ?? panic("playID does not exist") tmpPlay.updateTagline(tagline: tagline) return playID @@ -977,7 +944,7 @@ pub contract TopShot: NonFungibleToken { // Parameters: name: The name of the Set // // Returns: The ID of the created set - pub fun createSet(name: String): UInt32 { + access(all) fun createSet(name: String): UInt32 { // Create the new Set var newSet <- create Set(name: name) @@ -1004,7 +971,7 @@ pub contract TopShot: NonFungibleToken { // Returns: A reference to the Set with all of the fields // and methods exposed // - pub fun borrowSet(setID: UInt32): &Set { + access(all) view fun borrowSet(setID: UInt32): &Set { pre { TopShot.sets[setID] != nil: "Cannot borrow Set: The Set doesn't exist" } @@ -1020,7 +987,7 @@ pub contract TopShot: NonFungibleToken { // // Returns: The new series number // - pub fun startNewSeries(): UInt32 { + access(all) fun startNewSeries(): UInt32 { // End the current series and start a new one // by incrementing the TopShot series number TopShot.currentSeries = TopShot.currentSeries + UInt32(1) @@ -1032,8 +999,8 @@ pub contract TopShot: NonFungibleToken { // createSubeditionResource creates new SubeditionMap resource that // will be used to mint Moments with Subeditions - pub fun createSubeditionAdminResource() { - TopShot.account.save<@SubeditionAdmin>(<- create SubeditionAdmin(), to: TopShot.SubeditionAdminStoragePath()) + access(all) fun createSubeditionAdminResource() { + TopShot.account.storage.save<@SubeditionAdmin>(<- create SubeditionAdmin(), to: TopShot.SubeditionAdminStoragePath()) } // setMomentsSubedition saves which Subedition the Moment belongs to @@ -1043,8 +1010,8 @@ pub contract TopShot: NonFungibleToken { // setID: The ID of the Set that the Moment references // playID: The ID of the Play that the Moment references // - pub fun setMomentsSubedition(nftID: UInt64, subeditionID: UInt32, setID: UInt32, playID: UInt32) { - let subeditionAdmin = TopShot.account.borrow<&SubeditionAdmin>(from: TopShot.SubeditionAdminStoragePath()) + access(all) fun setMomentsSubedition(nftID: UInt64, subeditionID: UInt32, setID: UInt32, playID: UInt32) { + let subeditionAdmin = TopShot.account.storage.borrow<&SubeditionAdmin>(from: TopShot.SubeditionAdminStoragePath()) ?? panic("No subedition admin resource in storage") subeditionAdmin.setMomentsSubedition(nftID: nftID, subeditionID: subeditionID, setID: setID, playID: playID) @@ -1058,8 +1025,8 @@ pub contract TopShot: NonFungibleToken { // // Returns: the ID of the new Subedition object // - pub fun createSubedition(name:String, metadata:{String:String}): UInt32 { - let subeditionAdmin = TopShot.account.borrow<&SubeditionAdmin>(from: TopShot.SubeditionAdminStoragePath()) + access(all) fun createSubedition(name:String, metadata:{String:String}): UInt32 { + let subeditionAdmin = TopShot.account.storage.borrow<&SubeditionAdmin>(from: TopShot.SubeditionAdminStoragePath()) ?? panic("No subedition admin resource in storage") return subeditionAdmin.createSubedition(name:name, metadata:metadata) @@ -1067,7 +1034,7 @@ pub contract TopShot: NonFungibleToken { // createNewAdmin creates a new Admin resource // - pub fun createNewAdmin(): @Admin { + access(all) fun createNewAdmin(): @Admin { return <-create Admin() } } @@ -1075,12 +1042,9 @@ pub contract TopShot: NonFungibleToken { // This is the interface that users can cast their Moment Collection as // to allow others to deposit Moments into their Collection. It also allows for reading // the IDs of Moments in the Collection. - pub resource interface MomentCollectionPublic { - pub fun deposit(token: @NonFungibleToken.NFT) - pub fun batchDeposit(tokens: @NonFungibleToken.Collection) - pub fun getIDs(): [UInt64] - pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT - pub fun borrowMoment(id: UInt64): &TopShot.NFT? { + access(all) resource interface MomentCollectionPublic : NonFungibleToken.CollectionPublic { + access(all) fun batchDeposit(tokens: @{NonFungibleToken.Collection}) + access(all) fun borrowMoment(id: UInt64): &TopShot.NFT? { // If the result isn't nil, the id of the returned reference // should be the same as the argument to the function post { @@ -1093,35 +1057,62 @@ pub contract TopShot: NonFungibleToken { // Collection is a resource that every user who owns NFTs // will store in their account to manage their NFTS // - pub resource Collection: MomentCollectionPublic, NonFungibleToken.Provider, NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic, MetadataViews.ResolverCollection { + access(all) resource Collection: MomentCollectionPublic, NonFungibleToken.Collection { // Dictionary of Moment conforming tokens // NFT is a resource type with a UInt64 ID field - pub var ownedNFTs: @{UInt64: NonFungibleToken.NFT} + access(all) var ownedNFTs: @{UInt64: {NonFungibleToken.NFT}} init() { self.ownedNFTs <- {} } + // Return a list of NFT types that this receiver accepts + access(all) view fun getSupportedNFTTypes(): {Type: Bool} { + let supportedTypes: {Type: Bool} = {} + supportedTypes[Type<@TopShot.NFT>()] = true + return supportedTypes + } + + // Return whether or not the given type is accepted by the collection + // A collection that can accept any type should just return true by default + access(all) view fun isSupportedNFTType(type: Type): Bool { + if type == Type<@TopShot.NFT>() { + return true + } + return false + } + + // Return the amount of NFTs stored in the collection + access(all) view fun getLength(): Int { + return self.ownedNFTs.keys.length + } + + // Create an empty Collection for TopShot NFTs and return it to the caller + access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} { + return <- TopShot.createEmptyCollection(nftType: Type<@TopShot.NFT>()) + } + // withdraw removes an Moment from the Collection and moves it to the caller // // Parameters: withdrawID: The ID of the NFT // that is to be removed from the Collection // // returns: @NonFungibleToken.NFT the token that was withdrawn - pub fun withdraw(withdrawID: UInt64): @NonFungibleToken.NFT { + access(NonFungibleToken.Withdraw) fun withdraw(withdrawID: UInt64): @{NonFungibleToken.NFT} { // Borrow nft and check if locked - let nft = self.borrowNFT(id: withdrawID) + let nft = self.borrowNFT(withdrawID) + ?? panic("Cannot borrow: empty reference") if TopShotLocking.isLocked(nftRef: nft) { panic("Cannot withdraw: Moment is locked") } // Remove the nft from the Collection - let token <- self.ownedNFTs.remove(key: withdrawID) + let token <- self.ownedNFTs.remove(key: withdrawID) ?? panic("Cannot withdraw: Moment does not exist in the collection") emit Withdraw(id: token.id, from: self.owner?.address) - + // Return the withdrawn token return <-token } @@ -1133,7 +1124,7 @@ pub contract TopShot: NonFungibleToken { // Returns: @NonFungibleToken.Collection: A collection that contains // the withdrawn moments // - pub fun batchWithdraw(ids: [UInt64]): @NonFungibleToken.Collection { + access(NonFungibleToken.Withdraw) fun batchWithdraw(ids: [UInt64]): @{NonFungibleToken.Collection} { // Create a new empty Collection var batchCollection <- create Collection() @@ -1150,7 +1141,7 @@ pub contract TopShot: NonFungibleToken { // // Paramters: token: the NFT to be deposited in the collection // - pub fun deposit(token: @NonFungibleToken.NFT) { + access(all) fun deposit(token: @{NonFungibleToken.NFT}) { // Cast the deposited token as a TopShot NFT to make sure // it is the correct type @@ -1174,7 +1165,7 @@ pub contract TopShot: NonFungibleToken { // batchDeposit takes a Collection object as an argument // and deposits each contained NFT into this Collection - pub fun batchDeposit(tokens: @NonFungibleToken.Collection) { + access(all) fun batchDeposit(tokens: @{NonFungibleToken.Collection}) { // Get an array of the IDs to be deposited let keys = tokens.getIDs() @@ -1190,7 +1181,7 @@ pub contract TopShot: NonFungibleToken { // lock takes a token id and a duration in seconds and locks // the moment for that duration - pub fun lock(id: UInt64, duration: UFix64) { + access(NonFungibleToken.Update) fun lock(id: UInt64, duration: UFix64) { // Remove the nft from the Collection let token <- self.ownedNFTs.remove(key: id) ?? panic("Cannot lock: Moment does not exist in the collection") @@ -1204,7 +1195,7 @@ pub contract TopShot: NonFungibleToken { // batchLock takes an array of token ids and a duration in seconds // it iterates through the ids and locks each for the specified duration - pub fun batchLock(ids: [UInt64], duration: UFix64) { + access(NonFungibleToken.Update) fun batchLock(ids: [UInt64], duration: UFix64) { // Iterate through the ids and lock them for id in ids { self.lock(id: id, duration: duration) @@ -1213,7 +1204,7 @@ pub contract TopShot: NonFungibleToken { // unlock takes a token id and attempts to unlock it // TopShotLocking.unlockNFT contains business logic around unlock eligibility - pub fun unlock(id: UInt64) { + access(NonFungibleToken.Update) fun unlock(id: UInt64) { // Remove the nft from the Collection let token <- self.ownedNFTs.remove(key: id) ?? panic("Cannot lock: Moment does not exist in the collection") @@ -1227,15 +1218,40 @@ pub contract TopShot: NonFungibleToken { // batchUnlock takes an array of token ids // it iterates through the ids and unlocks each if they are eligible - pub fun batchUnlock(ids: [UInt64]) { + access(NonFungibleToken.Update) fun batchUnlock(ids: [UInt64]) { // Iterate through the ids and unlocks them for id in ids { self.unlock(id: id) } } + // destroyMoments destroys moments in this collection + // unlocks the moments if they are locked + // + // Parameters: ids: An array of NFT IDs + // to be destroyed from the Collection + access(NonFungibleToken.Update) fun destroyMoments(ids: [UInt64]) { + let topShotLockingAdmin = TopShot.account.storage.borrow<&TopShotLocking.Admin>(from: TopShotLocking.AdminStoragePath()) + ?? panic("No TopShotLocking admin resource in storage") + + for id in ids { + // Remove the nft from the Collection + let token <- self.ownedNFTs.remove(key: id) + ?? panic("Cannot destroy: Moment does not exist in collection: ".concat(id.toString())) + + // Emit a withdraw event here so that platforms do not have to understand TopShot-specific events to see ownership change + // A withdraw without a corresponding deposit means the NFT in question has no owner address + emit Withdraw(id: id, from: self.owner?.address) + + // does nothing if the moment is not locked + topShotLockingAdmin.unlockByID(id: id) + + destroy token + } + } + // getIDs returns an array of the IDs that are in the Collection - pub fun getIDs(): [UInt64] { + access(all) view fun getIDs(): [UInt64] { return self.ownedNFTs.keys } @@ -1250,21 +1266,8 @@ pub contract TopShot: NonFungibleToken { // not any topshot specific data. Please use borrowMoment to // read Moment data. // - pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT { - return (&self.ownedNFTs[id] as &NonFungibleToken.NFT?)! - } - - // Safe way to borrow a reference to an NFT that does not panic - // Also now part of the NonFungibleToken.PublicCollection interface - // - // Parameters: id: The ID of the NFT to get the reference for - // - // Returns: An optional reference to the desired NFT, will be nil if the passed ID does not exist - pub fun borrowNFTSafe(id: UInt64): &NonFungibleToken.NFT? { - if let nftRef = &self.ownedNFTs[id] as &NonFungibleToken.NFT? { - return nftRef - } - return nil + access(all) view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}? { + return &self.ownedNFTs[id] } // borrowMoment returns a borrowed reference to a Moment @@ -1277,29 +1280,17 @@ pub contract TopShot: NonFungibleToken { // Parameters: id: The ID of the NFT to get the reference for // // Returns: A reference to the NFT - pub fun borrowMoment(id: UInt64): &TopShot.NFT? { - if self.ownedNFTs[id] != nil { - let ref = (&self.ownedNFTs[id] as auth &NonFungibleToken.NFT?)! - return ref as! &TopShot.NFT - } else { - return nil - } + access(all) view fun borrowMoment(id: UInt64): &TopShot.NFT? { + return self.borrowNFT(id) as! &TopShot.NFT? } - pub fun borrowViewResolver(id: UInt64): &AnyResource{MetadataViews.Resolver} { - let nft = (&self.ownedNFTs[id] as auth &NonFungibleToken.NFT?)! - let topShotNFT = nft as! &TopShot.NFT - return topShotNFT as &AnyResource{MetadataViews.Resolver} + access(all) view fun borrowViewResolver(id: UInt64): &{ViewResolver.Resolver}? { + if let nft = &self.ownedNFTs[id] as &{NonFungibleToken.NFT}? { + return nft as &{ViewResolver.Resolver} + } + return nil } - // If a transaction destroys the Collection object, - // All the NFTs contained within are also destroyed! - // Much like when Damian Lillard destroys the hopes and - // dreams of the entire city of Houston. - // - destroy() { - destroy self.ownedNFTs - } } // ----------------------------------------------------------------------- @@ -1311,14 +1302,17 @@ pub contract TopShot: NonFungibleToken { // Once they have a Collection in their storage, they are able to receive // Moments in transactions. // - pub fun createEmptyCollection(): @NonFungibleToken.Collection { + access(all) fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection} { + if nftType != Type<@TopShot.NFT>() { + panic("NFT type is not supported") + } return <-create TopShot.Collection() } // getAllPlays returns all the plays in topshot // // Returns: An array of all the plays that have been created - pub fun getAllPlays(): [TopShot.Play] { + access(all) view fun getAllPlays(): [TopShot.Play] { return TopShot.playDatas.values } @@ -1327,7 +1321,7 @@ pub contract TopShot: NonFungibleToken { // Parameters: playID: The id of the Play that is being searched // // Returns: The metadata as a String to String mapping optional - pub fun getPlayMetaData(playID: UInt32): {String: String}? { + access(all) view fun getPlayMetaData(playID: UInt32): {String: String}? { return self.playDatas[playID]?.metadata } @@ -1340,7 +1334,7 @@ pub contract TopShot: NonFungibleToken { // field: The field to search for // // Returns: The metadata field as a String Optional - pub fun getPlayMetaDataByField(playID: UInt32, field: String): String? { + access(all) view fun getPlayMetaDataByField(playID: UInt32, field: String): String? { // Don't force a revert if the playID or field is invalid if let play = TopShot.playDatas[playID] { return play.metadata[field] @@ -1355,7 +1349,7 @@ pub contract TopShot: NonFungibleToken { // Parameters: setID: The id of the Set that is being searched // // Returns: The QuerySetData struct that has all the important information about the set - pub fun getSetData(setID: UInt32): QuerySetData? { + access(all) fun getSetData(setID: UInt32): QuerySetData? { if TopShot.sets[setID] == nil { return nil } else { @@ -1369,7 +1363,7 @@ pub contract TopShot: NonFungibleToken { // Parameters: setID: The id of the Set that is being searched // // Returns: The name of the Set - pub fun getSetName(setID: UInt32): String? { + access(all) view fun getSetName(setID: UInt32): String? { // Don't force a revert if the setID is invalid return TopShot.setDatas[setID]?.name } @@ -1380,7 +1374,7 @@ pub contract TopShot: NonFungibleToken { // Parameters: setID: The id of the Set that is being searched // // Returns: The series that the Set belongs to - pub fun getSetSeries(setID: UInt32): UInt32? { + access(all) view fun getSetSeries(setID: UInt32): UInt32? { // Don't force a revert if the setID is invalid return TopShot.setDatas[setID]?.series } @@ -1391,7 +1385,7 @@ pub contract TopShot: NonFungibleToken { // Parameters: setName: The name of the Set that is being searched // // Returns: An array of the IDs of the Set if it exists, or nil if doesn't - pub fun getSetIDsByName(setName: String): [UInt32]? { + access(all) fun getSetIDsByName(setName: String): [UInt32]? { var setIDs: [UInt32] = [] // Iterate through all the setDatas and search for the name @@ -1416,7 +1410,7 @@ pub contract TopShot: NonFungibleToken { // Parameters: setID: The id of the Set that is being searched // // Returns: An array of Play IDs - pub fun getPlaysInSet(setID: UInt32): [UInt32]? { + access(all) view fun getPlaysInSet(setID: UInt32): [UInt32]? { // Don't force a revert if the setID is invalid return TopShot.sets[setID]?.plays } @@ -1430,7 +1424,7 @@ pub contract TopShot: NonFungibleToken { // playID: The id of the Play that is being searched // // Returns: Boolean indicating if the edition is retired or not - pub fun isEditionRetired(setID: UInt32, playID: UInt32): Bool? { + access(all) fun isEditionRetired(setID: UInt32, playID: UInt32): Bool? { if let setdata = self.getSetData(setID: setID) { @@ -1454,7 +1448,7 @@ pub contract TopShot: NonFungibleToken { // Parameters: setID: The id of the Set that is being searched // // Returns: Boolean indicating if the Set is locked or not - pub fun isSetLocked(setID: UInt32): Bool? { + access(all) view fun isSetLocked(setID: UInt32): Bool? { // Don't force a revert if the setID is invalid return TopShot.sets[setID]?.locked } @@ -1467,7 +1461,7 @@ pub contract TopShot: NonFungibleToken { // // Returns: The total number of Moments // that have been minted from an edition - pub fun getNumMomentsInEdition(setID: UInt32, playID: UInt32): UInt32? { + access(all) fun getNumMomentsInEdition(setID: UInt32, playID: UInt32): UInt32? { if let setdata = self.getSetData(setID: setID) { // Read the numMintedPerPlay @@ -1486,8 +1480,8 @@ pub contract TopShot: NonFungibleToken { // // returns: UInt32? Subedition's ID if exists // - pub fun getMomentsSubedition(nftID: UInt64):UInt32? { - let subeditionAdmin = self.account.borrow<&SubeditionAdmin>(from: TopShot.SubeditionAdminStoragePath()) + access(all) view fun getMomentsSubedition(nftID: UInt64):UInt32? { + let subeditionAdmin = self.account.storage.borrow<&SubeditionAdmin>(from: TopShot.SubeditionAdminStoragePath()) ?? panic("No subedition admin resource in storage") return subeditionAdmin.getMomentsSubedition(nftID: nftID) @@ -1496,8 +1490,8 @@ pub contract TopShot: NonFungibleToken { // getAllSubeditions returns all the subeditions in topshot subeditionAdmin resource // // Returns: An array of all the subeditions that have been created - pub fun getAllSubeditions():[TopShot.Subedition] { - let subeditionAdmin = self.account.borrow<&SubeditionAdmin>(from: TopShot.SubeditionAdminStoragePath()) + access(all) view fun getAllSubeditions(): &[TopShot.Subedition] { + let subeditionAdmin = self.account.storage.borrow<&SubeditionAdmin>(from: TopShot.SubeditionAdminStoragePath()) ?? panic("No subedition admin resource in storage") return subeditionAdmin.subeditionDatas.values } @@ -1507,8 +1501,8 @@ pub contract TopShot: NonFungibleToken { // Parameters: subeditionID: The id of the Subedition that is being searched // // Returns: The Subedition struct - pub fun getSubeditionByID(subeditionID: UInt32):TopShot.Subedition { - let subeditionAdmin = self.account.borrow<&SubeditionAdmin>(from: TopShot.SubeditionAdminStoragePath()) + access(all) view fun getSubeditionByID(subeditionID: UInt32): &TopShot.Subedition { + let subeditionAdmin = self.account.storage.borrow<&SubeditionAdmin>(from: TopShot.SubeditionAdminStoragePath()) ?? panic("No subedition admin resource in storage") return subeditionAdmin.subeditionDatas[subeditionID]! } @@ -1518,19 +1512,19 @@ pub contract TopShot: NonFungibleToken { // // Returns: UInt32 // the next number in nextSubeditionID from the SubeditionAdmin resource - pub fun getNextSubeditionID():UInt32 { - let subeditionAdmin = self.account.borrow<&SubeditionAdmin>(from: TopShot.SubeditionAdminStoragePath()) + access(all) view fun getNextSubeditionID():UInt32 { + let subeditionAdmin = self.account.storage.borrow<&SubeditionAdmin>(from: TopShot.SubeditionAdminStoragePath()) ?? panic("No subedition admin resource in storage") return subeditionAdmin.nextSubeditionID } // SubeditionAdmin is a resource that allows Set to mint Moments with Subeditions // - pub struct Subedition { - pub let subeditionID: UInt32 + access(all) struct Subedition { + access(all) let subeditionID: UInt32 - pub let name: String + access(all) let name: String - pub let metadata: {String: String} + access(all) let metadata: {String: String} init(subeditionID: UInt32, name: String, metadata: {String: String}) { pre { @@ -1542,7 +1536,7 @@ pub contract TopShot: NonFungibleToken { } } - pub resource SubeditionAdmin { + access(all) resource SubeditionAdmin { // Map of number of already minted Moments using Subedition. // When a new Moment with Subedition is minted, 1 is added to the @@ -1570,7 +1564,7 @@ pub contract TopShot: NonFungibleToken { // // Returns: the ID of the new Subedition object // - pub fun createSubedition(name:String, metadata:{String:String}): UInt32 { + access(all) fun createSubedition(name:String, metadata:{String:String}): UInt32 { let newID = self.nextSubeditionID @@ -1591,7 +1585,7 @@ pub contract TopShot: NonFungibleToken { // // returns: UInt32? Subedition's ID if exists // - pub fun getMomentsSubedition(nftID: UInt64):UInt32? { + access(all) view fun getMomentsSubedition(nftID: UInt64):UInt32? { return self.momentsSubedition[nftID] } @@ -1605,7 +1599,7 @@ pub contract TopShot: NonFungibleToken { // // returns: UInt32 Number of Moments, already minted for this Subedition // - pub fun getNumberMintedPerSubedition(setID: UInt32, playID: UInt32, subeditionID: UInt32): UInt32 { + access(all) fun getNumberMintedPerSubedition(setID: UInt32, playID: UInt32, subeditionID: UInt32): UInt32 { let setPlaySubedition = setID.toString().concat(playID.toString()).concat(subeditionID.toString()) if !self.numberMintedPerSubedition.containsKey(setPlaySubedition) { self.numberMintedPerSubedition.insert(key: setPlaySubedition,UInt32(0)) @@ -1622,7 +1616,7 @@ pub contract TopShot: NonFungibleToken { // subeditionID: The ID of the Subedition using which moment will be minted // // - pub fun addToNumberMintedPerSubedition(setID: UInt32, playID: UInt32, subeditionID: UInt32) { + access(all) fun addToNumberMintedPerSubedition(setID: UInt32, playID: UInt32, subeditionID: UInt32) { let setPlaySubedition = setID.toString().concat(playID.toString()).concat(subeditionID.toString()) if !self.numberMintedPerSubedition.containsKey(setPlaySubedition) { @@ -1639,7 +1633,7 @@ pub contract TopShot: NonFungibleToken { // setID: The ID of the Set that the Moment references // playID: The ID of the Play that the Moment references // - pub fun setMomentsSubedition(nftID: UInt64, subeditionID: UInt32, setID: UInt32, playID: UInt32){ + access(all) fun setMomentsSubedition(nftID: UInt64, subeditionID: UInt32, setID: UInt32, playID: UInt32){ pre { !self.momentsSubedition.containsKey(nftID) : "Subedition for this moment already exists!" } @@ -1657,6 +1651,74 @@ pub contract TopShot: NonFungibleToken { } } + //------------------------------------------------------------ + // Contract MetadataViews + //------------------------------------------------------------ + + /// Return the metadata view types available for this contract + /// + access(all) view fun getContractViews(resourceType: Type?): [Type] { + return [Type(), Type(), Type()] + } + + /// Resolve this contract's metadata views + /// + access(all) view fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? { + post { + result == nil || result!.getType() == viewType: "The returned view must be of the given type or nil" + } + switch viewType { + case Type(): + return MetadataViews.NFTCollectionData( + storagePath: /storage/MomentCollection, + publicPath: /public/MomentCollection, + publicCollection: Type<&TopShot.Collection>(), + publicLinkedType: Type<&TopShot.Collection>(), + createEmptyCollectionFunction: (fun (): @{NonFungibleToken.Collection} { + return <-TopShot.createEmptyCollection(nftType: Type<@TopShot.NFT>()) + }) + ) + case Type(): + let bannerImage = MetadataViews.Media( + file: MetadataViews.HTTPFile( + url: "https://nbatopshot.com/static/img/top-shot-logo-horizontal-white.svg" + ), + mediaType: "image/svg+xml" + ) + let squareImage = MetadataViews.Media( + file: MetadataViews.HTTPFile( + url: "https://nbatopshot.com/static/img/og/og.png" + ), + mediaType: "image/png" + ) + return MetadataViews.NFTCollectionDisplay( + name: "NBA-Top-Shot", + description: "NBA Top Shot is your chance to own, sell, and trade official digital collectibles of the NBA and WNBA's greatest plays and players", + externalURL: MetadataViews.ExternalURL("https://nbatopshot.com"), + squareImage: squareImage, + bannerImage: bannerImage, + socials: { + "twitter": MetadataViews.ExternalURL("https://twitter.com/nbatopshot"), + "discord": MetadataViews.ExternalURL("https://discord.com/invite/nbatopshot"), + "instagram": MetadataViews.ExternalURL("https://www.instagram.com/nbatopshot") + } + ) + case Type(): + let royaltyReceiver: Capability<&{FungibleToken.Receiver}> = + getAccount(TopShot.RoyaltyAddress()).capabilities.get<&{FungibleToken.Receiver}>(MetadataViews.getRoyaltyReceiverPublicPath())! + return MetadataViews.Royalties( + [ + MetadataViews.Royalty( + receiver: royaltyReceiver, + cut: 0.05, + description: "NBATopShot marketplace royalty" + ) + ] + ) + } + return nil + } + // ----------------------------------------------------------------------- // TopShot initialization function @@ -1673,13 +1735,15 @@ pub contract TopShot: NonFungibleToken { self.totalSupply = 0 // Put a new Collection in storage - self.account.save<@Collection>(<- create Collection(), to: /storage/MomentCollection) + self.account.storage.save<@Collection>(<- create Collection(), to: /storage/MomentCollection) // Create a public capability for the Collection - self.account.link<&{MomentCollectionPublic}>(/public/MomentCollection, target: /storage/MomentCollection) + let cap = self.account.capabilities.storage.issue<&TopShot.Collection>(/storage/MomentCollection) + self.account.capabilities.publish(cap, at: /public/MomentCollection) + //self.account.link<&{MomentCollectionPublic}>(/public/MomentCollection, target: /storage/MomentCollection) // Put the Minter in storage - self.account.save<@Admin>(<- create Admin(), to: /storage/TopShotAdmin) + self.account.storage.save<@Admin>(<- create Admin(), to: /storage/TopShotAdmin) emit ContractInitialized() } diff --git a/contracts/dapper/TopShotLocking.cdc b/contracts/dapper/TopShotLocking.cdc index 11166f6..b8c1f01 100644 --- a/contracts/dapper/TopShotLocking.cdc +++ b/contracts/dapper/TopShotLocking.cdc @@ -1,16 +1,16 @@ import "NonFungibleToken" -pub contract TopShotLocking { +access(all) contract TopShotLocking { // ----------------------------------------------------------------------- // TopShotLocking contract Events // ----------------------------------------------------------------------- // Emitted when a Moment is locked - pub event MomentLocked(id: UInt64, duration: UFix64, expiryTimestamp: UFix64) + access(all) event MomentLocked(id: UInt64, duration: UFix64, expiryTimestamp: UFix64) // Emitted when a Moment is unlocked - pub event MomentUnlocked(id: UInt64) + access(all) event MomentUnlocked(id: UInt64) // Dictionary of locked NFTs // TopShot nft resource id is the key @@ -25,7 +25,7 @@ pub contract TopShotLocking { // Parameters: nftRef: A reference to the NFT resource // // Returns: true if NFT is locked - pub fun isLocked(nftRef: &NonFungibleToken.NFT): Bool { + access(all) view fun isLocked(nftRef: &{NonFungibleToken.NFT}): Bool { return self.lockedNFTs.containsKey(nftRef.id) } @@ -34,7 +34,7 @@ pub contract TopShotLocking { // Parameters: nftRef: A reference to the NFT resource // // Returns: unix timestamp - pub fun getLockExpiry(nftRef: &NonFungibleToken.NFT): UFix64 { + access(all) view fun getLockExpiry(nftRef: &{NonFungibleToken.NFT}): UFix64 { if !self.lockedNFTs.containsKey(nftRef.id) { panic("NFT is not locked") } @@ -47,7 +47,7 @@ pub contract TopShotLocking { // duration: number of seconds the NFT will be locked for // // Returns: the NFT resource - pub fun lockNFT(nft: @NonFungibleToken.NFT, duration: UFix64): @NonFungibleToken.NFT { + access(all) fun lockNFT(nft: @{NonFungibleToken.NFT}, duration: UFix64): @{NonFungibleToken.NFT} { let TopShotNFTType: Type = CompositeType("A.TOPSHOTADDRESS.TopShot.NFT")! if !nft.isInstance(TopShotNFTType) { panic("NFT is not a TopShot NFT") @@ -74,7 +74,7 @@ pub contract TopShotLocking { // Returns: the NFT resource // // NFT must be eligible for unlocking by an admin - pub fun unlockNFT(nft: @NonFungibleToken.NFT): @NonFungibleToken.NFT { + access(all) fun unlockNFT(nft: @{NonFungibleToken.NFT}): @{NonFungibleToken.NFT} { if !self.lockedNFTs.containsKey(nft.id) { // nft is not locked, short circuit and return the nft return <- nft @@ -101,7 +101,7 @@ pub contract TopShotLocking { // // Returns: array of ids // - pub fun getIDs(): [UInt64] { + access(all) view fun getIDs(): [UInt64] { return self.lockedNFTs.keys } @@ -111,7 +111,7 @@ pub contract TopShotLocking { // // Returns: a unix timestamp in seconds // - pub fun getExpiry(tokenID: UInt64): UFix64? { + access(all) view fun getExpiry(tokenID: UInt64): UFix64? { return self.lockedNFTs[tokenID] } @@ -119,17 +119,21 @@ pub contract TopShotLocking { // // Returns: an integer containing the number of locked tokens // - pub fun getLockedNFTsLength(): Int { + access(all) view fun getLockedNFTsLength(): Int { return self.lockedNFTs.length } + // The path to the TopShotLocking Admin resource belonging to the Account + // which the contract is deployed on + access(all) view fun AdminStoragePath() : StoragePath { return /storage/TopShotLockingAdmin} + // Admin is a special authorization resource that // allows the owner to override the lock on a moment // - pub resource Admin { + access(all) resource Admin { // createNewAdmin creates a new Admin resource // - pub fun createNewAdmin(): @Admin { + access(all) fun createNewAdmin(): @Admin { return <-create Admin() } @@ -137,12 +141,34 @@ pub contract TopShotLocking { // unlockable, overridding the expiry timestamp // the nft owner will still need to send an unlock transaction to unlock // - pub fun markNFTUnlockable(nftRef: &NonFungibleToken.NFT) { + access(all) fun markNFTUnlockable(nftRef: &{NonFungibleToken.NFT}) { TopShotLocking.unlockableNFTs[nftRef.id] = true } + access(all) fun unlockByID(id: UInt64) { + if !TopShotLocking.lockedNFTs.containsKey(id) { + // nft is not locked, do nothing + return + } + TopShotLocking.lockedNFTs.remove(key: id) + emit MomentUnlocked(id: id) + } + + // admin may alter the expiry of a lock on an NFT + access(all) fun setLockExpiryByID(id: UInt64, expiryTimestamp: UFix64) { + if expiryTimestamp < getCurrentBlock().timestamp { + panic("cannot set expiry in the past") + } + + let duration = expiryTimestamp - getCurrentBlock().timestamp + + TopShotLocking.lockedNFTs[id] = expiryTimestamp + + emit MomentLocked(id: id, duration: duration, expiryTimestamp: expiryTimestamp) + } + // unlocks all NFTs - pub fun unlockAll() { + access(all) fun unlockAll() { TopShotLocking.lockedNFTs = {} TopShotLocking.unlockableNFTs = {} } @@ -160,6 +186,6 @@ pub contract TopShotLocking { let admin <- create Admin() // Store it in private account storage in `init` so only the admin can use it - self.account.save(<-admin, to: /storage/TopShotLockingAdmin) + self.account.storage.save(<-admin, to: TopShotLocking.AdminStoragePath()) } } \ No newline at end of file diff --git a/contracts/dapper/offers/DapperOffersV2.cdc b/contracts/dapper/offers/DapperOffersV2.cdc index 38bc3d4..22267bf 100644 --- a/contracts/dapper/offers/DapperOffersV2.cdc +++ b/contracts/dapper/offers/DapperOffersV2.cdc @@ -2,6 +2,8 @@ import "OffersV2" import "FungibleToken" import "NonFungibleToken" import "Resolver" +import "ViewResolver" + // DapperOffersV2 // @@ -11,59 +13,62 @@ import "Resolver" // The DapperOffer resource contains the methods to add, remove, borrow and // get details on Offers contained within it. // -pub contract DapperOffersV2 { +access(all) contract DapperOffersV2 { + + access(all) entitlement Manager + access(all) entitlement ProxyManager // DapperOffersV2 // This contract has been deployed. // Event consumers can now expect events from this contract. // - pub event DapperOffersInitialized() + access(all) event DapperOffersInitialized() /// DapperOfferInitialized // A DapperOffer resource has been created. // - pub event DapperOfferInitialized(DapperOfferResourceId: UInt64) + access(all) event DapperOfferInitialized(DapperOfferResourceId: UInt64) // DapperOfferDestroyed // A DapperOffer resource has been destroyed. // Event consumers can now stop processing events from this resource. // - pub event DapperOfferDestroyed(DapperOfferResourceId: UInt64) + access(all) event DapperOfferDestroyed(DapperOfferResourceId: UInt64) // DapperOfferPublic // An interface providing a useful public interface to a Offer. // - pub resource interface DapperOfferPublic { + access(all) resource interface DapperOfferPublic { // getOfferIds // Get a list of Offer ids created by the resource. // - pub fun getOfferIds(): [UInt64] + access(all) fun getOfferIds(): [UInt64] // borrowOffer // Borrow an Offer to either accept the Offer or get details on the Offer. // - pub fun borrowOffer(offerId: UInt64): &OffersV2.Offer{OffersV2.OfferPublic}? + access(all) fun borrowOffer(offerId: UInt64): &{OffersV2.OfferPublic}? // cleanup // Remove an Offer // - pub fun cleanup(offerId: UInt64) + access(all) fun cleanup(offerId: UInt64) // addProxyCapability // Assign proxy capabilities (DapperOfferProxyManager) to an DapperOffer resource. // - pub fun addProxyCapability( + access(all) fun addProxyCapability( account: Address, - cap: Capability<&DapperOffer{DapperOffersV2.DapperOfferProxyManager}> + cap: Capability ) } // DapperOfferManager // An interface providing a management interface for an DapperOffer resource. // - pub resource interface DapperOfferManager { + access(all) resource interface DapperOfferManager { // createOffer // Allows the DapperOffer owner to create Offers. // - pub fun createOffer( - vaultRefCapability: Capability<&{FungibleToken.Provider, FungibleToken.Balance}>, + access(Manager) fun createOffer( + vaultRefCapability: Capability, nftReceiverCapability: Capability<&{NonFungibleToken.CollectionPublic}>, nftType: Type, amount: UFix64, @@ -76,21 +81,21 @@ pub contract DapperOffersV2 { // removeOffer // Allows the DapperOffer owner to remove offers // - pub fun removeOffer(offerId: UInt64) + access(Manager | ProxyManager) fun removeOffer(offerId: UInt64) } // DapperOfferProxyManager // An interface providing removeOffer on behalf of an DapperOffer owner. // - pub resource interface DapperOfferProxyManager { + access(all) resource interface DapperOfferProxyManager { // removeOffer // Allows the DapperOffer owner to remove offers // - pub fun removeOffer(offerId: UInt64) + access(Manager | ProxyManager) fun removeOffer(offerId: UInt64) // removeOfferFromProxy // Allows the DapperOffer proxy owner to remove offers // - pub fun removeOfferFromProxy(account: Address, offerId: UInt64) + access(ProxyManager) fun removeOfferFromProxy(account: Address, offerId: UInt64) } @@ -98,18 +103,18 @@ pub contract DapperOffersV2 { // A resource that allows its owner to manage a list of Offers, and anyone to interact with them // in order to query their details and accept the Offers for NFTs that they represent. // - pub resource DapperOffer : DapperOfferManager, DapperOfferPublic, DapperOfferProxyManager { + access(all) resource DapperOffer :DapperOfferManager, DapperOfferPublic, DapperOfferProxyManager { // The dictionary of Address to DapperOfferProxyManager capabilities. - access(self) var removeOfferCapability: {Address:Capability<&DapperOffer{DapperOffersV2.DapperOfferProxyManager}>} + access(self) var removeOfferCapability: {Address:Capability} // The dictionary of Offer uuids to Offer resources. access(self) var offers: @{UInt64:OffersV2.Offer} // addProxyCapability // Assign proxy capabilities (DapperOfferProxyManager) to an DapperOffer resource. // - pub fun addProxyCapability(account: Address, cap: Capability<&DapperOffer{DapperOffersV2.DapperOfferProxyManager}>) { + access(all) fun addProxyCapability(account: Address, cap: Capability) { pre { - cap.borrow() != nil: "Invalid admin capability" + cap != nil: "Invalid admin capability" } self.removeOfferCapability[account] = cap } @@ -117,23 +122,23 @@ pub contract DapperOffersV2 { // removeOfferFromProxy // Allows the DapperOffer proxy owner to remove offers // - pub fun removeOfferFromProxy(account: Address, offerId: UInt64) { + access(ProxyManager) fun removeOfferFromProxy(account: Address, offerId: UInt64) { pre { self.removeOfferCapability[account] != nil: "Cannot remove offers until the token admin has deposited the account registration capability" } - let adminRef = self.removeOfferCapability[account]!.borrow()! + let adminRef = self.removeOfferCapability[account]! - adminRef.removeOffer(offerId: offerId) + adminRef.borrow()!.removeOffer(offerId: offerId) } // createOffer // Allows the DapperOffer owner to create Offers. // - pub fun createOffer( - vaultRefCapability: Capability<&{FungibleToken.Provider, FungibleToken.Balance}>, + access(Manager) fun createOffer( + vaultRefCapability: Capability, nftReceiverCapability: Capability<&{NonFungibleToken.CollectionPublic}>, nftType: Type, amount: UFix64, @@ -165,23 +170,25 @@ pub contract DapperOffersV2 { // removeOffer // Remove an Offer that has not yet been accepted from the collection and destroy it. // - pub fun removeOffer(offerId: UInt64) { - destroy self.offers.remove(key: offerId) ?? panic("missing offer") + access(Manager | ProxyManager) fun removeOffer(offerId: UInt64) { + let offer <- self.offers.remove(key: offerId) ?? panic("missing offer") + // offer.customDestroy() + destroy offer } // getOfferIds // Returns an array of the Offer resource IDs that are in the collection // - pub fun getOfferIds(): [UInt64] { + access(all) view fun getOfferIds(): [UInt64] { return self.offers.keys } // borrowOffer // Returns a read-only view of the Offer for the given OfferID if it is contained by this collection. // - pub fun borrowOffer(offerId: UInt64): &OffersV2.Offer{OffersV2.OfferPublic}? { + access(all) view fun borrowOffer(offerId: UInt64): &{OffersV2.OfferPublic}? { if self.offers[offerId] != nil { - return (&self.offers[offerId] as &OffersV2.Offer{OffersV2.OfferPublic}?)! + return (&self.offers[offerId] as &{OffersV2.OfferPublic}?)! } else { return nil } @@ -192,7 +199,7 @@ pub contract DapperOffersV2 { // Anyone can call, but at present it only benefits the account owner to do so. // Kind purchasers can however call it if they like. // - pub fun cleanup(offerId: UInt64) { + access(all) fun cleanup(offerId: UInt64) { pre { self.offers[offerId] != nil: "could not find Offer with given id" } @@ -210,24 +217,20 @@ pub contract DapperOffersV2 { emit DapperOfferInitialized(DapperOfferResourceId: self.uuid) } - // destructor - // - destroy() { - destroy self.offers - // Let event consumers know that this storefront exists. - emit DapperOfferDestroyed(DapperOfferResourceId: self.uuid) - } + access(all) event ResourceDestroyed( + id: UInt64 = self.uuid + ) } // createDapperOffer // Make creating an DapperOffer publicly accessible. // - pub fun createDapperOffer(): @DapperOffer { + access(all) fun createDapperOffer(): @DapperOffer { return <-create DapperOffer() } - pub let DapperOffersStoragePath: StoragePath - pub let DapperOffersPublicPath: PublicPath + access(all) let DapperOffersStoragePath: StoragePath + access(all) let DapperOffersPublicPath: PublicPath init () { self.DapperOffersStoragePath = /storage/DapperOffersV2 @@ -235,4 +238,4 @@ pub contract DapperOffersV2 { emit DapperOffersInitialized() } -} +} \ No newline at end of file diff --git a/contracts/dapper/offers/OffersV2.cdc b/contracts/dapper/offers/OffersV2.cdc index 481758c..a6084fe 100644 --- a/contracts/dapper/offers/OffersV2.cdc +++ b/contracts/dapper/offers/OffersV2.cdc @@ -2,9 +2,10 @@ import "FungibleToken" import "NonFungibleToken" import "DapperUtilityCoin" import "MetadataViews" +import "ViewResolver" import "Resolver" -// OffersV2 +/// OffersV2 // // Contract holds the Offer resource and a public method to create them. // @@ -17,12 +18,12 @@ import "Resolver" // Marketplaces and other aggregators can watch for OfferAvailable events // and list offers of interest to logged in users. // -pub contract OffersV2 { +access(all) contract OffersV2 { // OfferAvailable // An Offer has been created and added to the users DapperOffer resource. // - pub event OfferAvailable( + access(all) event OfferAvailable( offerAddress: Address, offerId: UInt64, nftType: Type, @@ -39,7 +40,7 @@ pub contract OffersV2 { // The Offer has been resolved. The offer has either been accepted // by the NFT owner, or the offer has been removed and destroyed. // - pub event OfferCompleted( + access(all) event OfferCompleted( purchased: Bool, acceptingAddress: Address?, offerAddress: Address, @@ -59,9 +60,9 @@ pub contract OffersV2 { // A struct representing a recipient that must be sent a certain amount // of the payment when a NFT is sold. // - pub struct Royalty { - pub let receiver: Capability<&{FungibleToken.Receiver}> - pub let amount: UFix64 + access(all) struct Royalty { + access(all) let receiver: Capability<&{FungibleToken.Receiver}> + access(all) let amount: UFix64 init(receiver: Capability<&{FungibleToken.Receiver}>, amount: UFix64) { self.receiver = receiver @@ -72,23 +73,23 @@ pub contract OffersV2 { // OfferDetails // A struct containing Offers' data. // - pub struct OfferDetails { + access(all) struct OfferDetails { // The ID of the offer - pub let offerId: UInt64 + access(all) let offerId: UInt64 // The Type of the NFT - pub let nftType: Type + access(all) let nftType: Type // The Type of the FungibleToken that payments must be made in. - pub let paymentVaultType: Type + access(all) let paymentVaultType: Type // The Offer amount for the NFT - pub let offerAmount: UFix64 + access(all) let offerAmount: UFix64 // Flag to tracked the purchase state - pub var purchased: Bool + access(all) var purchased: Bool // This specifies the division of payment between recipients. - pub let royalties: [Royalty] + access(all) let royalties: [Royalty] // Used to hold Offer metadata and offer type information - pub let offerParamsString: {String: String} - pub let offerParamsUFix64: {String:UFix64} - pub let offerParamsUInt64: {String:UInt64} + access(all) let offerParamsString: {String: String} + access(all) let offerParamsUFix64: {String:UFix64} + access(all) let offerParamsUInt64: {String:UInt64} // setToPurchased // Irreversibly set this offer as purchased. @@ -124,33 +125,37 @@ pub contract OffersV2 { // OfferPublic // An interface providing a useful public interface to an Offer resource. // - pub resource interface OfferPublic { + access(all) resource interface OfferPublic { // accept // This will accept the offer if provided with the NFT id that matches the Offer // - pub fun accept( - item: @AnyResource{NonFungibleToken.INFT, MetadataViews.Resolver}, + access(all) fun accept( + item: @{NonFungibleToken.NFT, ViewResolver.Resolver}, receiverCapability: Capability<&{FungibleToken.Receiver}>, ): Void // getDetails // Return Offer details // - pub fun getDetails(): OfferDetails + access(all) fun getDetails(): OfferDetails } - pub resource Offer: OfferPublic { + access(all) resource Offer: OfferPublic { + + access(all) event ResourceDestroyed(purchased: Bool = self.details.purchased, offerId: UInt64 = self.details.offerId) + + // The OfferDetails struct of the Offer access(self) let details: OfferDetails // The vault which will handle the payment if the Offer is accepted. - access(contract) let vaultRefCapability: Capability<&{FungibleToken.Provider, FungibleToken.Balance}> + access(contract) let vaultRefCapability: Capability // Receiver address for the NFT when/if the Offer is accepted. access(contract) let nftReceiverCapability: Capability<&{NonFungibleToken.CollectionPublic}> // Resolver capability for the offer type access(contract) let resolverCapability: Capability<&{Resolver.ResolverPublic}> init( - vaultRefCapability: Capability<&{FungibleToken.Provider, FungibleToken.Balance}>, + vaultRefCapability: Capability, nftReceiverCapability: Capability<&{NonFungibleToken.CollectionPublic}>, nftType: Type, amount: UFix64, @@ -208,8 +213,8 @@ pub contract OffersV2 { // - Provided with a NFT matching the NFT id within the Offer details. // - Provided with a NFT matching the NFT Type within the Offer details. // - pub fun accept( - item: @AnyResource{NonFungibleToken.INFT, MetadataViews.Resolver}, + access(all) fun accept( + item: @{NonFungibleToken.NFT, ViewResolver.Resolver}, receiverCapability: Capability<&{FungibleToken.Receiver}>, ): Void { @@ -220,7 +225,7 @@ pub contract OffersV2 { let resolverCapability = self.resolverCapability.borrow() ?? panic("could not borrow resolver") let resolverResult = resolverCapability.checkOfferResolver( - item: &item as &AnyResource{NonFungibleToken.INFT, MetadataViews.Resolver}, + item: &item as &{NonFungibleToken.NFT, ViewResolver.Resolver}, offerParamsString: self.details.offerParamsString, offerParamsUInt64: self.details.offerParamsUInt64, offerParamsUFix64: self.details.offerParamsUFix64, @@ -231,7 +236,7 @@ pub contract OffersV2 { } self.details.setToPurchased() - let nft <- item as! @NonFungibleToken.NFT + let nft <- item as! @{NonFungibleToken.NFT} let nftId: UInt64 = nft.id self.nftReceiverCapability.borrow()!.deposit(token: <- nft) @@ -251,8 +256,8 @@ pub contract OffersV2 { // If a DUC vault is being used for payment we must assert that no DUC is leaking from the transactions. let isDucVault = self.vaultRefCapability.isInstance( - Type>() - ) + Type>() + ) // todo: check if this is correct if isDucVault { assert(self.vaultRefCapability.borrow()!.balance == initalDucSupply, message: "DUC is leaking") @@ -278,14 +283,14 @@ pub contract OffersV2 { // getDetails // Return Offer details // - pub fun getDetails(): OfferDetails { + access(all) view fun getDetails(): OfferDetails { return self.details } // getRoyaltyInfo // Return royalty details // - pub fun getRoyaltyInfo(): {Address:UFix64} { + access(all) view fun getRoyaltyInfo(): {Address:UFix64} { let royaltyInfo: {Address:UFix64} = {} for royalty in self.details.royalties { @@ -293,31 +298,11 @@ pub contract OffersV2 { } return royaltyInfo; } - - destroy() { - if !self.details.purchased { - emit OfferCompleted( - purchased: self.details.purchased, - acceptingAddress: nil, - offerAddress: self.nftReceiverCapability.address, - offerId: self.details.offerId, - nftType: self.details.nftType, - offerAmount: self.details.offerAmount, - royalties: self.getRoyaltyInfo(), - offerType: self.details.offerParamsString["_type"] ?? "unknown", - offerParamsString: self.details.offerParamsString, - offerParamsUFix64: self.details.offerParamsUFix64, - offerParamsUInt64: self.details.offerParamsUInt64, - paymentVaultType: self.details.paymentVaultType, - nftId: nil, - ) - } - } } // makeOffer - pub fun makeOffer( - vaultRefCapability: Capability<&{FungibleToken.Provider, FungibleToken.Balance}>, + access(all) fun makeOffer( + vaultRefCapability: Capability, nftReceiverCapability: Capability<&{NonFungibleToken.CollectionPublic}>, nftType: Type, amount: UFix64, @@ -340,5 +325,4 @@ pub contract OffersV2 { ) return <-newOfferResource } -} - \ No newline at end of file +} \ No newline at end of file diff --git a/contracts/dapper/offers/Resolver.cdc b/contracts/dapper/offers/Resolver.cdc index af49d65..9310331 100644 --- a/contracts/dapper/offers/Resolver.cdc +++ b/contracts/dapper/offers/Resolver.cdc @@ -1,20 +1,27 @@ import "NonFungibleToken" import "MetadataViews" import "TopShot" +import "ViewResolver" -pub contract Resolver { +// Resolver +// +// Contract holds the Offer exchange resolution rules. +// +// When an Offer is created a ResolverType is included. The ResolverType is also +// passed into checkOfferResolver() from the Offers contract on exchange validation +access(all) contract Resolver { // Current list of supported resolution rules. - pub enum ResolverType: UInt8 { - pub case NFT - pub case TopShotEdition - pub case MetadataViewsEditions + access(all) enum ResolverType: UInt8 { + access(all) case NFT + access(all) case TopShotEdition + access(all) case MetadataViewsEditions } // Public resource interface that defines a method signature for checkOfferResolver // which is used within the Resolver resource for offer acceptance validation - pub resource interface ResolverPublic { - pub fun checkOfferResolver( - item: &AnyResource{NonFungibleToken.INFT, MetadataViews.Resolver}, + access(all) resource interface ResolverPublic { + access(all) fun checkOfferResolver( + item: &{NonFungibleToken.NFT, ViewResolver.Resolver}, offerParamsString: {String:String}, offerParamsUInt64: {String:UInt64}, offerParamsUFix64: {String:UFix64}): Bool @@ -22,19 +29,19 @@ pub contract Resolver { // Resolver resource holds the Offer exchange resolution rules. - pub resource OfferResolver: ResolverPublic { + access(all) resource OfferResolver: ResolverPublic { // checkOfferResolver // Holds the validation rules for resolver each type of supported ResolverType // Function returns TRUE if the provided nft item passes the criteria for exchange - pub fun checkOfferResolver( - item: &AnyResource{NonFungibleToken.INFT, MetadataViews.Resolver}, + access(all) fun checkOfferResolver( + item: &{NonFungibleToken.NFT, ViewResolver.Resolver}, offerParamsString: {String:String}, offerParamsUInt64: {String:UInt64}, offerParamsUFix64: {String:UFix64}): Bool { if offerParamsString["resolver"] == ResolverType.NFT.rawValue.toString() { assert(item.id.toString() == offerParamsString["nftId"], message: "item NFT does not have specified ID") return true - } else if offerParamsString["resolver"] == ResolverType.TopShotEdition.rawValue.toString() { + } else if offerParamsString["resolver"] == ResolverType.TopShotEdition.rawValue.toString() { // // Get the Top Shot specific metadata for this NFT let view = item.resolveView(Type())! let metadata = view as! TopShot.TopShotMomentMetadataView @@ -64,7 +71,7 @@ pub contract Resolver { } - pub fun createResolver(): @OfferResolver { + access(all) fun createResolver(): @OfferResolver { return <-create OfferResolver() } } \ No newline at end of file diff --git a/contracts/emerald-city/FLOAT.cdc b/contracts/emerald-city/FLOAT.cdc index 7323aa5..b049022 100644 --- a/contracts/emerald-city/FLOAT.cdc +++ b/contracts/emerald-city/FLOAT.cdc @@ -22,36 +22,39 @@ import "NonFungibleToken" import "MetadataViews" import "FungibleToken" import "FlowToken" -import "FindViews" +// import "FindViews" import "ViewResolver" -pub contract FLOAT: NonFungibleToken, ViewResolver { +access(all) contract FLOAT: NonFungibleToken, ViewResolver { + + access(all) entitlement EventOwner + access(all) entitlement EventsOwner /***********************************************/ /******************** PATHS ********************/ /***********************************************/ - pub let FLOATCollectionStoragePath: StoragePath - pub let FLOATCollectionPublicPath: PublicPath - pub let FLOATEventsStoragePath: StoragePath - pub let FLOATEventsPublicPath: PublicPath - pub let FLOATEventsPrivatePath: PrivatePath + access(all) let FLOATCollectionStoragePath: StoragePath + access(all) let FLOATCollectionPublicPath: PublicPath + access(all) let FLOATEventsStoragePath: StoragePath + access(all) let FLOATEventsPublicPath: PublicPath + access(all) let FLOATEventsPrivatePath: PrivatePath /************************************************/ /******************** EVENTS ********************/ /************************************************/ - pub event ContractInitialized() - pub event FLOATMinted(id: UInt64, eventHost: Address, eventId: UInt64, eventImage: String, recipient: Address, serial: UInt64) - pub event FLOATClaimed(id: UInt64, eventHost: Address, eventId: UInt64, eventImage: String, eventName: String, recipient: Address, serial: UInt64) - pub event FLOATDestroyed(id: UInt64, eventHost: Address, eventId: UInt64, eventImage: String, serial: UInt64) - pub event FLOATTransferred(id: UInt64, eventHost: Address, eventId: UInt64, newOwner: Address?, serial: UInt64) - pub event FLOATPurchased(id: UInt64, eventHost: Address, eventId: UInt64, recipient: Address, serial: UInt64) - pub event FLOATEventCreated(eventId: UInt64, description: String, host: Address, image: String, name: String, url: String) - pub event FLOATEventDestroyed(eventId: UInt64, host: Address, name: String) + access(all) event ContractInitialized() + access(all) event FLOATMinted(id: UInt64, eventHost: Address, eventId: UInt64, eventImage: String, recipient: Address, serial: UInt64) + access(all) event FLOATClaimed(id: UInt64, eventHost: Address, eventId: UInt64, eventImage: String, eventName: String, recipient: Address, serial: UInt64) + access(all) event FLOATDestroyed(id: UInt64, eventHost: Address, eventId: UInt64, eventImage: String, serial: UInt64) + access(all) event FLOATTransferred(id: UInt64, eventHost: Address, eventId: UInt64, newOwner: Address?, serial: UInt64) + access(all) event FLOATPurchased(id: UInt64, eventHost: Address, eventId: UInt64, recipient: Address, serial: UInt64) + access(all) event FLOATEventCreated(eventId: UInt64, description: String, host: Address, image: String, name: String, url: String) + access(all) event FLOATEventDestroyed(eventId: UInt64, host: Address, name: String) - pub event Deposit(id: UInt64, to: Address?) - pub event Withdraw(id: UInt64, from: Address?) + access(all) event Deposit(id: UInt64, to: Address?) + access(all) event Withdraw(id: UInt64, from: Address?) /***********************************************/ /******************** STATE ********************/ @@ -59,10 +62,10 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { // The total amount of FLOATs that have ever been // created (does not go down when a FLOAT is destroyed) - pub var totalSupply: UInt64 + access(all) var totalSupply: UInt64 // The total amount of FLOATEvents that have ever been // created (does not go down when a FLOATEvent is destroyed) - pub var totalFLOATEvents: UInt64 + access(all) var totalFLOATEvents: UInt64 /***********************************************/ /**************** FUNCTIONALITY ****************/ @@ -70,10 +73,10 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { // A helpful wrapper to contain an address, // the id of a FLOAT, and its serial - pub struct TokenIdentifier { - pub let id: UInt64 - pub let address: Address - pub let serial: UInt64 + access(all) struct TokenIdentifier { + access(all) let id: UInt64 + access(all) let address: Address + access(all) let serial: UInt64 init(_id: UInt64, _address: Address, _serial: UInt64) { self.id = _id @@ -82,9 +85,9 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { } } - pub struct TokenInfo { - pub let path: PublicPath - pub let price: UFix64 + access(all) struct TokenInfo { + access(all) let path: PublicPath + access(all) let price: UFix64 init(_path: PublicPath, _price: UFix64) { self.path = _path @@ -93,50 +96,54 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { } // Represents a FLOAT - pub resource NFT: NonFungibleToken.INFT, MetadataViews.Resolver { + access(all) resource NFT: NonFungibleToken.NFT { // The `uuid` of this resource - pub let id: UInt64 + access(all) let id: UInt64 // Some of these are also duplicated on the event, // but it's necessary to put them here as well // in case the FLOATEvent host deletes the event - pub let dateReceived: UFix64 - pub let eventDescription: String - pub let eventHost: Address - pub let eventId: UInt64 - pub let eventImage: String - pub let eventName: String - pub let originalRecipient: Address - pub let serial: UInt64 + access(all) let dateReceived: UFix64 + access(all) let eventDescription: String + access(all) let eventHost: Address + access(all) let eventId: UInt64 + access(all) let eventImage: String + access(all) let eventName: String + access(all) let originalRecipient: Address + access(all) let serial: UInt64 // A capability that points to the FLOATEvents this FLOAT is from. // There is a chance the event host unlinks their event from // the public, in which case it's impossible to know details // about the event. Which is fine, since we store the // crucial data to know about the FLOAT in the FLOAT itself. - pub let eventsCap: Capability<&FLOATEvents{FLOATEventsPublic, MetadataViews.ResolverCollection}> + access(all) let eventsCap: Capability<&FLOATEvents> + + access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} { + return <- FLOAT.createEmptyCollection(nftType: Type<@FLOAT.NFT>()) + } // Helper function to get the metadata of the event // this FLOAT is from. - pub fun getEventRef(): &FLOATEvent{FLOATEventPublic}? { - if let events: &FLOATEvents{FLOATEventsPublic, MetadataViews.ResolverCollection} = self.eventsCap.borrow() { + access(all) fun getEventRef(): &FLOATEvent? { + if let events: &FLOATEvents = self.eventsCap.borrow() { return events.borrowPublicEventRef(eventId: self.eventId) } return nil } - pub fun getExtraMetadata(): {String: AnyStruct} { - if let event: &FLOATEvent{FLOATEventPublic} = self.getEventRef() { - return event.getExtraFloatMetadata(serial: self.serial) + access(all) fun getExtraMetadata(): {String: AnyStruct} { + if let eventRef: &FLOATEvent = self.getEventRef() { + return eventRef.getExtraFloatMetadata(serial: self.serial) } return {} } - pub fun getSpecificExtraMetadata(key: String): AnyStruct? { + access(all) fun getSpecificExtraMetadata(key: String): AnyStruct? { return self.getExtraMetadata()[key] } - pub fun getImage(): String { + access(all) fun getImage(): String { if let extraEventMetadata: {String: AnyStruct} = self.getEventRef()?.getExtraMetadata() { if FLOAT.extraMetadataToStrOpt(extraEventMetadata, "visibilityMode") == "picture" { return self.eventImage @@ -157,7 +164,7 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { } // This is for the MetdataStandard - pub fun getViews(): [Type] { + access(all) view fun getViews(): [Type] { let supportedViews = [ Type(), Type(), @@ -169,15 +176,15 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { Type() ] - if self.getEventRef()?.transferrable == false { - supportedViews.append(Type()) - } + // if self.getEventRef()?.transferrable == false { + // supportedViews.append(Type()) + // } return supportedViews } // This is for the MetdataStandard - pub fun resolveView(_ view: Type): AnyStruct? { + access(all) fun resolveView(_ view: Type): AnyStruct? { switch view { case Type(): return MetadataViews.Display( @@ -188,7 +195,7 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { case Type(): return MetadataViews.Royalties([ MetadataViews.Royalty( - recepient: getAccount(0x5643fd47a29770e7).getCapability<&FlowToken.Vault{FungibleToken.Receiver}>(/public/flowTokenReceiver), + receiver: getAccount(0x5643fd47a29770e7).capabilities.get<&{FungibleToken.Receiver}>(/public/flowTokenReceiver), cut: 0.05, // 5% royalty on secondary sales description: "Emerald City DAO receives a 5% royalty from secondary sales because this NFT was created using FLOAT (https://floats.city/), a proof of attendance platform created by Emerald City DAO." ) @@ -196,9 +203,9 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { case Type(): return MetadataViews.ExternalURL("https://floats.city/".concat(self.owner!.address.toString()).concat("/float/").concat(self.id.toString())) case Type(): - return FLOAT.resolveView(view) + return FLOAT.resolveContractView(resourceType: Type<@FLOAT.NFT>(), viewType: Type()) case Type(): - return FLOAT.resolveView(view) + return FLOAT.resolveContractView(resourceType: Type<@FLOAT.NFT>(), viewType: Type()) case Type(): return MetadataViews.Serial( self.serial @@ -209,24 +216,37 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { _address: self.owner!.address, _serial: self.serial ) - case Type(): - if self.getEventRef()?.transferrable == false { - return FindViews.SoulBound( - "This FLOAT is soulbound because the event host toggled off transferring." - ) - } - return nil + // case Type(): + // if self.getEventRef()?.transferrable == false { + // return FindViews.SoulBound( + // "This FLOAT is soulbound because the event host toggled off transferring." + // ) + // } + // return nil case Type(): let traitsView: MetadataViews.Traits = MetadataViews.dictToTraits(dict: self.getExtraMetadata(), excludedNames: nil) - if let eventRef: &FLOATEvent{FLOATEventPublic} = self.getEventRef() { + if let eventRef: &FLOATEvent = self.getEventRef() { let eventExtraMetadata: {String: AnyStruct} = eventRef.getExtraMetadata() - let certificateType: MetadataViews.Trait = MetadataViews.Trait(name: "certificateType", value: eventExtraMetadata["certificateType"], displayType: nil, rarity: nil) - traitsView.addTrait(certificateType) - + // certificate type doesn't apply if it's a picture FLOAT + if FLOAT.extraMetadataToStrOpt(eventExtraMetadata, "visibilityMode") == "certificate" { + let certificateType: MetadataViews.Trait = MetadataViews.Trait(name: "certificateType", value: eventExtraMetadata["certificateType"], displayType: nil, rarity: nil) + traitsView.addTrait(certificateType) + } + + let serial: MetadataViews.Trait = MetadataViews.Trait(name: "serial", value: self.serial, displayType: nil, rarity: nil) + traitsView.addTrait(serial) + let originalRecipient: MetadataViews.Trait = MetadataViews.Trait(name: "originalRecipient", value: self.originalRecipient, displayType: nil, rarity: nil) + traitsView.addTrait(originalRecipient) + let eventCreator: MetadataViews.Trait = MetadataViews.Trait(name: "eventCreator", value: self.eventHost, displayType: nil, rarity: nil) + traitsView.addTrait(eventCreator) let eventType: MetadataViews.Trait = MetadataViews.Trait(name: "eventType", value: eventExtraMetadata["eventType"], displayType: nil, rarity: nil) traitsView.addTrait(eventType) + let dateReceived: MetadataViews.Trait = MetadataViews.Trait(name: "dateMinted", value: self.dateReceived, displayType: "Date", rarity: nil) + traitsView.addTrait(dateReceived) + let eventId: MetadataViews.Trait = MetadataViews.Trait(name: "eventId", value: self.eventId, displayType: nil, rarity: nil) + traitsView.addTrait(eventId) } return traitsView @@ -247,8 +267,7 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { self.serial = _serial // Stores a capability to the FLOATEvents of its creator - self.eventsCap = getAccount(_eventHost) - .getCapability<&FLOATEvents{FLOATEventsPublic, MetadataViews.ResolverCollection}>(FLOAT.FLOATEventsPublicPath) + self.eventsCap = getAccount(_eventHost).capabilities.get<&FLOATEvents>(FLOAT.FLOATEventsPublicPath) emit FLOATMinted( id: self.id, @@ -262,33 +281,22 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { FLOAT.totalSupply = FLOAT.totalSupply + 1 } - destroy() { - emit FLOATDestroyed( - id: self.id, - eventHost: self.eventHost, - eventId: self.eventId, - eventImage: self.eventImage, - serial: self.serial - ) - } - } - - // A public interface for people to call into our Collection - pub resource interface CollectionPublic { - pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT - pub fun borrowFLOAT(id: UInt64): &NFT? - pub fun borrowViewResolver(id: UInt64): &{MetadataViews.Resolver} - pub fun deposit(token: @NonFungibleToken.NFT) - pub fun getIDs(): [UInt64] - pub fun getAllIDs(): [UInt64] - pub fun ownedIdsFromEvent(eventId: UInt64): [UInt64] + // destroy() { + // emit FLOATDestroyed( + // id: self.id, + // eventHost: self.eventHost, + // eventId: self.eventId, + // eventImage: self.eventImage, + // serial: self.serial + // ) + // } } // A Collection that holds all of the users FLOATs. // Withdrawing is not allowed. You can only transfer. - pub resource Collection: NonFungibleToken.Provider, NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic, MetadataViews.ResolverCollection, CollectionPublic { + access(all) resource Collection: NonFungibleToken.Collection { // Maps a FLOAT id to the FLOAT itself - pub var ownedNFTs: @{UInt64: NonFungibleToken.NFT} + access(all) var ownedNFTs: @{UInt64: {NonFungibleToken.NFT}} // Maps an eventId to the ids of FLOATs that // this user owns from that event. It's possible // for it to be out of sync until June 2022 spork, @@ -296,7 +304,7 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { access(self) var events: {UInt64: {UInt64: Bool}} // Deposits a FLOAT to the collection - pub fun deposit(token: @NonFungibleToken.NFT) { + access(all) fun deposit(token: @{NonFungibleToken.NFT}) { let nft <- token as! @NFT let id = nft.id let eventId = nft.eventId @@ -309,12 +317,12 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { self.events[eventId]!.insert(key: id, true) } - emit Deposit(id: id, to: self.owner!.address) - emit FLOATTransferred(id: id, eventHost: nft.eventHost, eventId: nft.eventId, newOwner: self.owner!.address, serial: nft.serial) + emit Deposit(id: id, to: self.owner?.address) + emit FLOATTransferred(id: id, eventHost: nft.eventHost, eventId: nft.eventId, newOwner: self.owner?.address, serial: nft.serial) self.ownedNFTs[id] <-! nft } - pub fun withdraw(withdrawID: UInt64): @NonFungibleToken.NFT { + access(NonFungibleToken.Withdraw) fun withdraw(withdrawID: UInt64): @{NonFungibleToken.NFT} { let token <- self.ownedNFTs.remove(key: withdrawID) ?? panic("You do not own this FLOAT in your collection") let nft <- token as! @NFT @@ -326,19 +334,19 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { // FLOAT to be transferrable. Secondary marketplaces will use this // withdraw function, so if the FLOAT is not transferrable, // you can't sell it there. - if let floatEvent: &FLOATEvent{FLOATEventPublic} = nft.getEventRef() { + if let floatEvent: &FLOATEvent = nft.getEventRef() { assert( floatEvent.transferrable, message: "This FLOAT is not transferrable." ) } - emit Withdraw(id: withdrawID, from: self.owner!.address) + emit Withdraw(id: withdrawID, from: self.owner?.address) emit FLOATTransferred(id: withdrawID, eventHost: nft.eventHost, eventId: nft.eventId, newOwner: nil, serial: nft.serial) return <- nft } - pub fun delete(id: UInt64) { + access(NonFungibleToken.Update) fun delete(id: UInt64) { let token <- self.ownedNFTs.remove(key: id) ?? panic("You do not own this FLOAT in your collection") let nft <- token as! @NFT @@ -351,19 +359,7 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { // Only returns the FLOATs for which we can still // access data about their event. - pub fun getIDs(): [UInt64] { - let ids: [UInt64] = [] - for key in self.ownedNFTs.keys { - let nftRef = self.borrowFLOAT(id: key)! - if nftRef.eventsCap.check() { - ids.append(key) - } - } - return ids - } - - // Returns all the FLOATs ids - pub fun getAllIDs(): [UInt64] { + access(all) view fun getIDs(): [UInt64] { return self.ownedNFTs.keys } @@ -375,7 +371,7 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { // from `ownedNFTs` (not possible after June 2022 spork), // but this makes sure the returned // ids are all actually owned by this account. - pub fun ownedIdsFromEvent(eventId: UInt64): [UInt64] { + access(all) fun ownedIdsFromEvent(eventId: UInt64): [UInt64] { let answer: [UInt64] = [] if let idsInEvent = self.events[eventId]?.keys { for id in idsInEvent { @@ -387,32 +383,53 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { return answer } - pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT { - return (&self.ownedNFTs[id] as &NonFungibleToken.NFT?)! + access(all) view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}? { + return (&self.ownedNFTs[id] as &{NonFungibleToken.NFT}?) + } + + access(all) view fun getLength(): Int { + return self.ownedNFTs.keys.length + } + + /// getSupportedNFTTypes returns a list of NFT types that this receiver accepts + access(all) view fun getSupportedNFTTypes(): {Type: Bool} { + let supportedTypes: {Type: Bool} = {} + supportedTypes[Type<@FLOAT.NFT>()] = true + return supportedTypes + } + + /// Returns whether or not the given type is accepted by the collection + /// A collection that can accept any type should just return true by default + access(all) view fun isSupportedNFTType(type: Type): Bool { + if type == Type<@FLOAT.NFT>() { + return true + } else { + return false + } } - pub fun borrowFLOAT(id: UInt64): &NFT? { - if self.ownedNFTs[id] != nil { - let ref = (&self.ownedNFTs[id] as auth &NonFungibleToken.NFT?)! - return ref as! &NFT + access(all) fun borrowFLOAT(id: UInt64): &NFT? { + if let nft = &self.ownedNFTs[id] as &{NonFungibleToken.NFT}? { + return nft as! &NFT } return nil } - pub fun borrowViewResolver(id: UInt64): &{MetadataViews.Resolver} { - let tokenRef = (&self.ownedNFTs[id] as auth &NonFungibleToken.NFT?)! - let nftRef = tokenRef as! &NFT - return nftRef as &{MetadataViews.Resolver} + access(all) view fun borrowViewResolver(id: UInt64): &{ViewResolver.Resolver}? { + if let nft = &self.ownedNFTs[id] as &{NonFungibleToken.NFT}? { + return nft as &{ViewResolver.Resolver} + } + return nil + } + + access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} { + return <- FLOAT.createEmptyCollection(nftType: Type<@FLOAT.NFT>()) } init() { self.ownedNFTs <- {} self.events = {} } - - destroy() { - destroy self.ownedNFTs - } } // An interface that every "verifier" must implement. @@ -420,7 +437,7 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { // for example, a "time limit," or a "limited" number of // FLOATs that can be claimed. // All the current verifiers can be seen inside FLOATVerifiers.cdc - pub struct interface IVerifier { + access(all) struct interface IVerifier { // A function every verifier must implement. // Will have `assert`s in it to make sure // the user fits some criteria. @@ -428,60 +445,60 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { } // A public interface to read the FLOATEvent - pub resource interface FLOATEventPublic { - pub var claimable: Bool - pub let dateCreated: UFix64 - pub let description: String - pub let eventId: UInt64 - pub let host: Address - pub let image: String - pub let name: String - pub var totalSupply: UInt64 - pub var transferrable: Bool - pub let url: String - - pub fun claim(recipient: &Collection, params: {String: AnyStruct}) - pub fun purchase(recipient: &Collection, params: {String: AnyStruct}, payment: @FungibleToken.Vault) - - pub fun getExtraMetadata(): {String: AnyStruct} - pub fun getSpecificExtraMetadata(key: String): AnyStruct? - pub fun getVerifiers(): {String: [{IVerifier}]} - pub fun getPrices(): {String: TokenInfo}? - pub fun getExtraFloatMetadata(serial: UInt64): {String: AnyStruct} - pub fun getSpecificExtraFloatMetadata(serial: UInt64, key: String): AnyStruct? - pub fun getClaims(): {UInt64: TokenIdentifier} - pub fun getSerialsUserClaimed(address: Address): [UInt64] - pub fun userHasClaimed(address: Address): Bool - pub fun userCanMint(address: Address): Bool + access(all) resource interface FLOATEventPublic { + access(all) var claimable: Bool + access(all) let dateCreated: UFix64 + access(all) let description: String + access(all) let eventId: UInt64 + access(all) let host: Address + access(all) let image: String + access(all) let name: String + access(all) var totalSupply: UInt64 + access(all) var transferrable: Bool + access(all) let url: String + + access(all) fun claim(recipient: &Collection, params: {String: AnyStruct}) + access(all) fun purchase(recipient: &Collection, params: {String: AnyStruct}, payment: @{FungibleToken.Vault}) + + access(all) fun getExtraMetadata(): {String: AnyStruct} + access(all) fun getSpecificExtraMetadata(key: String): AnyStruct? + access(all) fun getVerifiers(): {String: [{IVerifier}]} + access(all) fun getPrices(): {String: TokenInfo}? + access(all) fun getExtraFloatMetadata(serial: UInt64): {String: AnyStruct} + access(all) fun getSpecificExtraFloatMetadata(serial: UInt64, key: String): AnyStruct? + access(all) fun getClaims(): {UInt64: TokenIdentifier} + access(all) fun getSerialsUserClaimed(address: Address): [UInt64] + access(all) fun userHasClaimed(address: Address): Bool + access(all) fun userCanMint(address: Address): Bool } // // FLOATEvent // - pub resource FLOATEvent: FLOATEventPublic, MetadataViews.Resolver { + access(all) resource FLOATEvent { // Whether or not users can claim from our event (can be toggled // at any time) - pub var claimable: Bool - pub let dateCreated: UFix64 - pub let description: String + access(all) var claimable: Bool + access(all) let dateCreated: UFix64 + access(all) let description: String // This is equal to this resource's uuid - pub let eventId: UInt64 + access(all) let eventId: UInt64 // Who created this FLOAT Event - pub let host: Address + access(all) let host: Address // The image of the FLOAT Event - pub let image: String + access(all) let image: String // The name of the FLOAT Event - pub let name: String + access(all) let name: String // The total number of FLOATs that have been // minted from this event - pub var totalSupply: UInt64 + access(all) var totalSupply: UInt64 // Whether or not the FLOATs that users own // from this event can be transferred on the // FLOAT platform itself (transferring allowed // elsewhere) - pub var transferrable: Bool + access(all) var transferrable: Bool // A url of where the event took place - pub let url: String + access(all) let url: String // A list of verifiers this FLOAT Event contains. // Will be used every time someone "claims" a FLOAT // to see if they pass the requirements @@ -499,19 +516,19 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { access(self) var groups: {String: Bool} // Type: Admin Toggle - pub fun toggleClaimable(): Bool { + access(EventOwner) fun toggleClaimable(): Bool { self.claimable = !self.claimable return self.claimable } // Type: Admin Toggle - pub fun toggleTransferrable(): Bool { + access(EventOwner) fun toggleTransferrable(): Bool { self.transferrable = !self.transferrable return self.transferrable } // Type: Admin Toggle - pub fun toggleVisibilityMode() { + access(EventOwner) fun toggleVisibilityMode() { if let currentVisibilityMode: String = FLOAT.extraMetadataToStrOpt(self.getExtraMetadata(), "visibilityMode") { if currentVisibilityMode == "certificate" { self.extraMetadata["visibilityMode"] = "picture" @@ -529,11 +546,12 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { let userClaims: {Address: [UInt64]} = {} self.extraMetadata["userClaims"] = userClaims } - let e = (&self.extraMetadata["userClaims"] as auth &AnyStruct?)! - let claims = e as! &{Address: [UInt64]} - - if let specificUserClaims: [UInt64] = claims[address] { - claims[address]!.append(serial) + let e = (&self.extraMetadata["userClaims"] as auth(Mutate) &AnyStruct?)! + let claims = e as! auth(Mutate) &{Address: [UInt64]} + let claimsByAddress = claims[address] as! auth(Mutate) &[UInt64]? + + if let specificUserClaims: auth(Mutate) &[UInt64] = claimsByAddress { + specificUserClaims.append(serial) } else { claims[address] = [serial] } @@ -545,8 +563,8 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { let extraFloatMetadatas: {UInt64: AnyStruct} = {} self.extraMetadata["extraFloatMetadatas"] = extraFloatMetadatas } - let e = (&self.extraMetadata["extraFloatMetadatas"] as auth &AnyStruct?)! - let extraFloatMetadatas = e as! &{UInt64: AnyStruct} + let e = (&self.extraMetadata["extraFloatMetadatas"] as auth(Mutate)&AnyStruct?)! + let extraFloatMetadatas = e as! auth(Mutate) &{UInt64: AnyStruct} extraFloatMetadatas[serial] = metadata } @@ -556,22 +574,22 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { let extraFloatMetadatas: {UInt64: AnyStruct} = {} self.extraMetadata["extraFloatMetadatas"] = extraFloatMetadatas } - let e = (&self.extraMetadata["extraFloatMetadatas"] as auth &AnyStruct?)! - let extraFloatMetadatas = e as! &{UInt64: AnyStruct} + let e = (&self.extraMetadata["extraFloatMetadatas"] as auth(Mutate) &AnyStruct?)! + let extraFloatMetadatas = e as! auth(Mutate) &{UInt64: AnyStruct} if extraFloatMetadatas[serial] == nil { let extraFloatMetadata: {String: AnyStruct} = {} extraFloatMetadatas[serial] = extraFloatMetadata } - let f = (&extraFloatMetadatas[serial] as auth &AnyStruct?)! - let extraFloatMetadata = e as! &{String: AnyStruct} + let f = (extraFloatMetadatas[serial] as! auth(Mutate) &AnyStruct?)! + let extraFloatMetadata = e as! auth(Mutate) &{String: AnyStruct} extraFloatMetadata[key] = value } // Type: Getter // Description: Get extra metadata on a specific FLOAT from this event - pub fun getExtraFloatMetadata(serial: UInt64): {String: AnyStruct} { + access(all) view fun getExtraFloatMetadata(serial: UInt64): {String: AnyStruct} { if self.extraMetadata["extraFloatMetadatas"] != nil { if let e: {UInt64: AnyStruct} = self.extraMetadata["extraFloatMetadatas"]! as? {UInt64: AnyStruct} { if e[serial] != nil { @@ -586,13 +604,13 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { // Type: Getter // Description: Get specific extra metadata on a specific FLOAT from this event - pub fun getSpecificExtraFloatMetadata(serial: UInt64, key: String): AnyStruct? { + access(all) view fun getSpecificExtraFloatMetadata(serial: UInt64, key: String): AnyStruct? { return self.getExtraFloatMetadata(serial: serial)[key] } // Type: Getter // Description: Returns claim info of all the serials - pub fun getClaims(): {UInt64: TokenIdentifier} { + access(all) view fun getClaims(): {UInt64: TokenIdentifier} { return self.currentHolders } @@ -600,14 +618,14 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { // Description: Will return an array of all the serials a user claimed. // Most of the time this will be a maximum length of 1 because most // events only allow 1 claim per user. - pub fun getSerialsUserClaimed(address: Address): [UInt64] { + access(all) view fun getSerialsUserClaimed(address: Address): [UInt64] { var serials: [UInt64] = [] if let userClaims: {Address: [UInt64]} = self.getSpecificExtraMetadata(key: "userClaims") as! {Address: [UInt64]}? { serials = userClaims[address] ?? [] } // take into account claims during FLOATv1 if let oldClaim: TokenIdentifier = self.claimed[address] { - serials.append(oldClaim.serial) + serials = serials.concat([oldClaim.serial]) } return serials } @@ -615,37 +633,37 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { // Type: Getter // Description: Returns true if the user has either claimed // or been minted at least one float from this event - pub fun userHasClaimed(address: Address): Bool { + access(all) view fun userHasClaimed(address: Address): Bool { return self.getSerialsUserClaimed(address: address).length >= 1 } // Type: Getter // Description: Get extra metadata on this event - pub fun getExtraMetadata(): {String: AnyStruct} { + access(all) view fun getExtraMetadata(): {String: AnyStruct} { return self.extraMetadata } // Type: Getter // Description: Get specific extra metadata on this event - pub fun getSpecificExtraMetadata(key: String): AnyStruct? { + access(all) view fun getSpecificExtraMetadata(key: String): AnyStruct? { return self.extraMetadata[key] } // Type: Getter // Description: Checks if a user can mint a new FLOAT from this event - pub fun userCanMint(address: Address): Bool { + access(all) view fun userCanMint(address: Address): Bool { if let allows: Bool = self.getSpecificExtraMetadata(key: "allowMultipleClaim") as! Bool? { if allows || self.getSerialsUserClaimed(address: address).length == 0 { return true } } - return false + return !self.userHasClaimed(address: address) } // Type: Getter // Description: Gets all the verifiers that will be used // for claiming - pub fun getVerifiers(): {String: [{IVerifier}]} { + access(all) view fun getVerifiers(): {String: [{IVerifier}]} { return self.verifiers } @@ -653,13 +671,13 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { // Description: Returns a dictionary whose key is a token identifier // and value is the path to that token and price of the FLOAT in that // currency - pub fun getPrices(): {String: TokenInfo}? { + access(all) view fun getPrices(): {String: TokenInfo}? { return self.extraMetadata["prices"] as! {String: TokenInfo}? } // Type: Getter // Description: For MetadataViews - pub fun getViews(): [Type] { + access(all) view fun getViews(): [Type] { return [ Type() ] @@ -667,7 +685,7 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { // Type: Getter // Description: For MetadataViews - pub fun resolveView(_ view: Type): AnyStruct? { + access(all) view fun resolveView(_ view: Type): AnyStruct? { switch view { case Type(): return MetadataViews.Display( @@ -685,7 +703,7 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { // If the event owner directly mints to a user, it does not // run the verifiers on the user. It bypasses all of them. // Return the id of the FLOAT it minted. - pub fun mint(recipient: &Collection{NonFungibleToken.CollectionPublic}, optExtraFloatMetadata: {String: AnyStruct}?): UInt64 { + access(EventOwner) fun mint(recipient: &Collection, optExtraFloatMetadata: {String: AnyStruct}?): UInt64 { pre { self.userCanMint(address: recipient.owner!.address): "Only 1 FLOAT allowed per user, and this user already claimed their FLOAT!" } @@ -724,7 +742,7 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { // Description: Will get run by the public, so verifies // the user can mint access(self) fun verifyAndMint(recipient: &Collection, params: {String: AnyStruct}): UInt64 { - params["event"] = &self as &FLOATEvent{FLOATEventPublic} + params["event"] = &self as &FLOATEvent params["claimee"] = recipient.owner!.address // Runs a loop over all the verifiers that this FLOAT Events @@ -734,7 +752,7 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { let typedModules = (&self.verifiers[identifier] as &[{IVerifier}]?)! var i = 0 while i < typedModules.length { - let verifier = &typedModules[i] as &{IVerifier} + let verifier = typedModules[i] verifier.verify(params) i = i + 1 } @@ -768,7 +786,7 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { // For example, the FLOAT platform allows event hosts // to specify a secret phrase. That secret phrase will // be passed in the `params`. - pub fun claim(recipient: &Collection, params: {String: AnyStruct}) { + access(all) fun claim(recipient: &Collection, params: {String: AnyStruct}) { pre { self.getPrices() == nil: "You need to purchase this FLOAT." @@ -779,7 +797,7 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { self.verifyAndMint(recipient: recipient, params: params) } - pub fun purchase(recipient: &Collection, params: {String: AnyStruct}, payment: @FungibleToken.Vault) { + access(all) fun purchase(recipient: &Collection, params: {String: AnyStruct}, payment: @{FungibleToken.Vault}) { pre { self.getPrices() != nil: "Don't call this function. The FLOAT is free. Call the claim function instead." @@ -795,8 +813,7 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { let paymentType: String = payment.getType().identifier let tokenInfo: TokenInfo = self.getPrices()![paymentType]! - let EventHostVault = getAccount(self.host).getCapability(tokenInfo.path) - .borrow<&{FungibleToken.Receiver}>() + let EventHostVault = getAccount(self.host).capabilities.borrow<&{FungibleToken.Receiver}>(tokenInfo.path) ?? panic("Could not borrow the &{FungibleToken.Receiver} from the event host.") assert( @@ -804,8 +821,7 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { message: "The event host's path is not associated with the intended token." ) - let EmeraldCityVault = getAccount(emeraldCityTreasury).getCapability(tokenInfo.path) - .borrow<&{FungibleToken.Receiver}>() + let EmeraldCityVault = getAccount(emeraldCityTreasury).capabilities.borrow<&{FungibleToken.Receiver}>(tokenInfo.path) ?? panic("Could not borrow the &{FungibleToken.Receiver} from Emerald City's Vault.") assert( @@ -854,17 +870,17 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { emit FLOATEventCreated(eventId: self.eventId, description: self.description, host: self.host, image: self.image, name: self.name, url: self.url) } - destroy() { - emit FLOATEventDestroyed(eventId: self.eventId, host: self.host, name: self.name) - } + // destroy() { + // emit FLOATEventDestroyed(eventId: self.eventId, host: self.host, name: self.name) + // } } // DEPRECATED - pub resource Group { - pub let id: UInt64 - pub let name: String - pub let image: String - pub let description: String + access(all) resource Group { + access(all) let id: UInt64 + access(all) let name: String + access(all) let image: String + access(all) let description: String access(self) var events: {UInt64: Bool} init() { self.id = 0 @@ -878,16 +894,9 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { // // FLOATEvents // - pub resource interface FLOATEventsPublic { - // Public Getters - pub fun borrowPublicEventRef(eventId: UInt64): &FLOATEvent{FLOATEventPublic}? - pub fun getAllEvents(): {UInt64: String} - pub fun getIDs(): [UInt64] - pub fun borrowViewResolver(id: UInt64): &{MetadataViews.Resolver} - } // A "Collection" of FLOAT Events - pub resource FLOATEvents: FLOATEventsPublic, MetadataViews.ResolverCollection { + access(all) resource FLOATEvents { // All the FLOAT Events this collection stores access(self) var events: @{UInt64: FLOATEvent} // DEPRECATED @@ -913,7 +922,7 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { // certificateImage: Must either be nil or a String type // backImage: The IPFS CID of what will display on the back of your FLOAT. Must either be nil or a String type // eventType: Must either be nil or a String type - pub fun createEvent( + access(EventsOwner) fun createEvent( claimable: Bool, description: String, image: String, @@ -938,7 +947,7 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { let typedVerifiers: {String: [{IVerifier}]} = {} for verifier in verifiers { - let identifier = verifier.getType().identifier + let identifier: String = verifier.getType().identifier if typedVerifiers[identifier] == nil { typedVerifiers[identifier] = [verifier] } else { @@ -961,35 +970,35 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { _url: url, _verifiers: typedVerifiers ) - let eventId = FLOATEvent.eventId + let eventId: UInt64 = FLOATEvent.eventId self.events[eventId] <-! FLOATEvent return eventId } // Deletes an event. - pub fun deleteEvent(eventId: UInt64) { + access(EventsOwner) fun deleteEvent(eventId: UInt64) { let eventRef = self.borrowEventRef(eventId: eventId) ?? panic("This FLOAT does not exist.") destroy self.events.remove(key: eventId) } - pub fun borrowEventRef(eventId: UInt64): &FLOATEvent? { - return &self.events[eventId] as &FLOATEvent? + access(EventsOwner) fun borrowEventRef(eventId: UInt64): auth(EventOwner) &FLOATEvent? { + return &self.events[eventId] } // Get a public reference to the FLOATEvent // so you can call some helpful getters - pub fun borrowPublicEventRef(eventId: UInt64): &FLOATEvent{FLOATEventPublic}? { - return &self.events[eventId] as &FLOATEvent{FLOATEventPublic}? + access(all) fun borrowPublicEventRef(eventId: UInt64): &FLOATEvent? { + return &self.events[eventId] as &FLOATEvent? } - pub fun getIDs(): [UInt64] { + access(all) fun getIDs(): [UInt64] { return self.events.keys } // Maps the eventId to the name of that // event. Just a kind helper. - pub fun getAllEvents(): {UInt64: String} { + access(all) fun getAllEvents(): {UInt64: String} { let answer: {UInt64: String} = {} for id in self.events.keys { let ref = (&self.events[id] as &FLOATEvent?)! @@ -998,32 +1007,23 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { return answer } - pub fun borrowViewResolver(id: UInt64): &{MetadataViews.Resolver} { - return (&self.events[id] as &{MetadataViews.Resolver}?)! - } - init() { self.events <- {} self.groups <- {} } - - destroy() { - destroy self.events - destroy self.groups - } } - pub fun createEmptyCollection(): @Collection { + access(all) fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection} { return <- create Collection() } - pub fun createEmptyFLOATEventCollection(): @FLOATEvents { + access(all) fun createEmptyFLOATEventCollection(): @FLOATEvents { return <- create FLOATEvents() } // A function to validate expected FLOAT metadata that must be in a // certain format as to not cause aborts during expected casting - pub fun validateExtraFloatMetadata(data: {String: AnyStruct}): Bool { + access(all) fun validateExtraFloatMetadata(data: {String: AnyStruct}): Bool { if data.containsKey("medalType") { let medalType: String? = FLOAT.extraMetadataToStrOpt(data, "medalType") if medalType == nil || (medalType != "gold" && medalType != "silver" && medalType != "bronze" && medalType != "participation") { @@ -1044,7 +1044,7 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { // So we force unwrap due to the dictionary, then unwrap the value within. // It will never abort because we have checked for nil above, which checks // for both types of nil. - pub fun extraMetadataToStrOpt(_ dict: {String: AnyStruct}, _ key: String): String? { + access(all) fun extraMetadataToStrOpt(_ dict: {String: AnyStruct}, _ key: String): String? { // `dict[key] == nil` means: // 1. the key doesn't exist // 2. the value for the key is nil @@ -1059,7 +1059,7 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { /// @return An array of Types defining the implemented views. This value will be used by /// developers to know which parameter to pass to the resolveView() method. /// - pub fun getViews(): [Type] { + access(all) fun getViews(): [Type] { return [ Type(), Type() @@ -1071,18 +1071,16 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { /// @param view: The Type of the desired view. /// @return A structure representing the requested view. /// - pub fun resolveView(_ view: Type): AnyStruct? { - switch view { + access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? { + switch viewType { case Type(): return MetadataViews.NFTCollectionData( storagePath: FLOAT.FLOATCollectionStoragePath, publicPath: FLOAT.FLOATCollectionPublicPath, - providerPath: /private/FLOATCollectionPrivatePath, - publicCollection: Type<&Collection{CollectionPublic}>(), - publicLinkedType: Type<&Collection{CollectionPublic, NonFungibleToken.CollectionPublic, NonFungibleToken.Receiver, MetadataViews.ResolverCollection}>(), - providerLinkedType: Type<&Collection{CollectionPublic, NonFungibleToken.CollectionPublic, NonFungibleToken.Provider, MetadataViews.ResolverCollection}>(), - createEmptyCollectionFunction: (fun (): @NonFungibleToken.Collection { - return <- FLOAT.createEmptyCollection() + publicCollection: Type<&Collection>(), + publicLinkedType: Type<&Collection>(), + createEmptyCollectionFunction: (fun(): @{NonFungibleToken.Collection} { + return <- FLOAT.createEmptyCollection(nftType: Type<@FLOAT.NFT>()) }) ) case Type(): @@ -1113,6 +1111,13 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { return nil } + access(all) view fun getContractViews(resourceType: Type?): [Type] { + return [ + Type(), + Type() + ] + } + init() { self.totalSupply = 0 self.totalFLOATEvents = 0 @@ -1126,19 +1131,19 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { // delete later - if self.account.borrow<&FLOAT.Collection>(from: FLOAT.FLOATCollectionStoragePath) == nil { - self.account.save(<- FLOAT.createEmptyCollection(), to: FLOAT.FLOATCollectionStoragePath) - self.account.link<&FLOAT.Collection{NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic, MetadataViews.ResolverCollection, FLOAT.CollectionPublic}> - (FLOAT.FLOATCollectionPublicPath, target: FLOAT.FLOATCollectionStoragePath) + if self.account.storage.borrow<&FLOAT.Collection>(from: FLOAT.FLOATCollectionStoragePath) == nil { + self.account.storage.save(<- create Collection(), to: FLOAT.FLOATCollectionStoragePath) + let collectionCap = self.account.capabilities.storage.issue<&FLOAT.Collection>(FLOAT.FLOATCollectionStoragePath) + self.account.capabilities.publish(collectionCap, at: FLOAT.FLOATCollectionPublicPath) } - if self.account.borrow<&FLOAT.FLOATEvents>(from: FLOAT.FLOATEventsStoragePath) == nil { - self.account.save(<- FLOAT.createEmptyFLOATEventCollection(), to: FLOAT.FLOATEventsStoragePath) - self.account.link<&FLOAT.FLOATEvents{FLOAT.FLOATEventsPublic, MetadataViews.ResolverCollection}> - (FLOAT.FLOATEventsPublicPath, target: FLOAT.FLOATEventsStoragePath) + if self.account.storage.borrow<&FLOAT.FLOATEvents>(from: FLOAT.FLOATEventsStoragePath) == nil { + self.account.storage.save(<- FLOAT.createEmptyFLOATEventCollection(), to: FLOAT.FLOATEventsStoragePath) + let eventsCap = self.account.capabilities.storage.issue<&FLOAT.FLOATEvents>(FLOAT.FLOATEventsStoragePath) + self.account.capabilities.publish(eventsCap, at: FLOAT.FLOATEventsPublicPath) } - let FLOATEvents = self.account.borrow<&FLOAT.FLOATEvents>(from: FLOAT.FLOATEventsStoragePath) + let FLOATEvents = self.account.storage.borrow(from: FLOAT.FLOATEventsStoragePath) ?? panic("Could not borrow the FLOATEvents from the signer.") var verifiers: [{FLOAT.IVerifier}] = [] @@ -1158,4 +1163,4 @@ pub contract FLOAT: NonFungibleToken, ViewResolver { FLOATEvents.createEvent(claimable: true, description: "Test description for a Discord meeting. This is soooo fun! Woohoo!", image: "bafybeifpsnwb2vkz4p6nxhgsbwgyslmlfd7jyicx5ukbj3tp7qsz7myzrq", name: "Discord Meeting", transferrable: true, url: "", verifiers: verifiers, allowMultipleClaim: false, certificateType: "ticket", visibilityMode: "picture", extraMetadata: extraMetadata) } } - + \ No newline at end of file diff --git a/contracts/example/ExampleNFT.cdc b/contracts/example/ExampleNFT.cdc new file mode 100644 index 0000000..ec56c3f --- /dev/null +++ b/contracts/example/ExampleNFT.cdc @@ -0,0 +1,419 @@ +/* +* +* This is an example implementation of a Flow Non-Fungible Token +* It is not part of the official standard but it assumed to be +* similar to how many NFTs would implement the core functionality. +* +* This contract does not implement any sophisticated classification +* system for its NFTs. It defines a simple NFT with minimal metadata. +* +*/ + +import "NonFungibleToken" +import "MetadataViews" +import "ViewResolver" +import "FungibleToken" + +// THIS CONTRACT IS FOR TESTING PURPOSES ONLY! +access(all) contract ExampleNFT: ViewResolver { + + access(all) var totalSupply: UInt64 + + access(all) event ContractInitialized() + access(all) event Withdraw(id: UInt64, from: Address?) + access(all) event Deposit(id: UInt64, to: Address?) + access(all) event Mint(id: UInt64) + + access(all) event CollectionCreated(id: UInt64) + access(all) event CollectionDestroyed(id: UInt64) + + access(all) let CollectionStoragePath: StoragePath + access(all) let CollectionPublicPath: PublicPath + access(all) let MinterStoragePath: StoragePath + access(all) let MinterPublicPath: PublicPath + + access(all) resource NFT: NonFungibleToken.NFT, ViewResolver.Resolver { + access(all) let id: UInt64 + + access(all) let name: String + access(all) let description: String + access(all) let thumbnail: String + access(self) var royalties: [MetadataViews.Royalty] + + access(all) view fun getID(): UInt64 { + return self.id + } + + init( + id: UInt64, + name: String, + description: String, + thumbnail: String, + royalties: [MetadataViews.Royalty] + ) { + self.id = id + self.name = name + self.description = description + self.thumbnail = thumbnail + self.royalties = royalties + + emit Mint(id: self.id) + } + + access(Mutate) fun setRoyalties(_ royalties: [MetadataViews.Royalty]) { + self.royalties = royalties + } + + access(all) view fun getViews(): [Type] { + return [ + Type(), + Type(), + Type(), + Type(), + Type(), + Type(), + Type(), + Type() + ] + } + + access(all) fun resolveView(_ view: Type): AnyStruct? { + switch view { + case Type(): + return MetadataViews.Display( + name: self.name, + description: self.description, + thumbnail: MetadataViews.HTTPFile( + url: self.thumbnail + ) + ) + case Type(): + // There is no max number of NFTs that can be minted from this contract + // so the max edition field value is set to nil + let editionName = self.id % 2 == 0 ? "Example NFT Edition (even)" : "Example NFT Edition (odd)" + let editionInfo = MetadataViews.Edition(name: editionName, number: self.id, max: nil) + let editionList: [MetadataViews.Edition] = [editionInfo] + return MetadataViews.Editions( + editionList + ) + case Type(): + return MetadataViews.Serial( + self.id + ) + case Type(): + return MetadataViews.Royalties( + self.royalties + ) + case Type(): + return MetadataViews.ExternalURL("https://example-nft.onflow.org/".concat(self.id.toString())) + case Type(): + return MetadataViews.NFTCollectionData( + storagePath: ExampleNFT.CollectionStoragePath, + publicPath: ExampleNFT.CollectionPublicPath, + publicCollection: Type<&ExampleNFT.Collection>(), + publicLinkedType: Type<&ExampleNFT.Collection>(), + createEmptyCollectionFunction: (fun (): @{NonFungibleToken.Collection} { + return <-ExampleNFT.createEmptyCollection() + }) + ) + case Type(): + let media = MetadataViews.Media( + file: MetadataViews.HTTPFile( + url: "https://assets.website-files.com/5f6294c0c7a8cdd643b1c820/5f6294c0c7a8cda55cb1c936_Flow_Wordmark.svg" + ), + mediaType: "image/svg+xml" + ) + return MetadataViews.NFTCollectionDisplay( + name: "The Example Collection", + description: "This collection is used as an example to help you develop your next Flow NFT.", + externalURL: MetadataViews.ExternalURL("https://example-nft.onflow.org"), + squareImage: media, + bannerImage: media, + socials: { + "twitter": MetadataViews.ExternalURL("https://twitter.com/flow_blockchain") + } + ) + case Type(): + let dict: {String: AnyStruct} = { + "name": self.name, + "even": self.id % 2 == 0, + "id": self.id + } + return MetadataViews.dictToTraits(dict: dict, excludedNames: []) + } + return nil + } + + access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} { + return <- ExampleNFT.createEmptyCollection() + } + } + + access(all) resource interface ExampleNFTCollectionPublic: NonFungibleToken.Collection { + access(all) fun deposit(token: @{NonFungibleToken.NFT}) + access(all) fun borrowExampleNFT(id: UInt64): &ExampleNFT.NFT? { + post { + (result == nil) || (result?.id == id): + "Cannot borrow ExampleNFT reference: the ID of the returned reference is incorrect" + } + } + } + + access(all) resource Collection: ExampleNFTCollectionPublic { + access(all) event ResourceDestroyed(id: UInt64 = self.uuid) + + // dictionary of NFT conforming tokens + // NFT is a resource type with an `UInt64` ID field + access(all) var ownedNFTs: @{UInt64: {NonFungibleToken.NFT}} + + init () { + self.ownedNFTs <- {} + emit CollectionCreated(id: self.uuid) + } + + access(all) view fun getDefaultStoragePath(): StoragePath? { + return ExampleNFT.CollectionStoragePath + } + + access(all) view fun getDefaultPublicPath(): PublicPath? { + return ExampleNFT.CollectionPublicPath + } + + access(all) view fun getLength(): Int { + return self.ownedNFTs.length + } + + access(all) view fun getSupportedNFTTypes(): {Type: Bool} { + return { + Type<@ExampleNFT.NFT>(): true + } + } + + access(all) view fun getIDs(): [UInt64] { + return self.ownedNFTs.keys + } + + access(all) view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}? { + return &self.ownedNFTs[id] + } + + access(all) view fun isSupportedNFTType(type: Type): Bool { + return type == Type<@ExampleNFT.NFT>() + } + + access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} { + return <- ExampleNFT.createEmptyCollection() + } + + // withdraw removes an NFT from the collection and moves it to the caller + access(NonFungibleToken.Withdraw) fun withdraw(withdrawID: UInt64): @{NonFungibleToken.NFT} { + let token <- self.ownedNFTs.remove(key: withdrawID) ?? panic("missing NFT") + + emit Withdraw(id: token.id, from: self.owner?.address) + + return <-token + } + + // deposit takes a NFT and adds it to the collections dictionary + // and adds the ID to the id array + access(all) fun deposit(token: @{NonFungibleToken.NFT}) { + let token <- token as! @ExampleNFT.NFT + + let id: UInt64 = token.id + + // add the new token to the dictionary which removes the old one + let oldToken <- self.ownedNFTs[id] <- token + + emit Deposit(id: id, to: self.owner?.address) + + destroy oldToken + } + + access(all) fun borrowExampleNFT(id: UInt64): &ExampleNFT.NFT? { + if self.ownedNFTs[id] != nil { + // Create an authorized reference to allow downcasting + let ref = (&self.ownedNFTs[id] as &{NonFungibleToken.NFT}?)! + return ref as! &ExampleNFT.NFT + } + + return nil + } + } + + // public function that anyone can call to create a new empty collection + access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} { + return <- create Collection() + } + + // Resource that an admin or something similar would own to be + // able to mint new NFTs + // + access(all) resource NFTMinter { + // mintNFT mints a new NFT with a new ID + // and deposit it in the recipients collection using their collection reference + access(all) fun mintNFT( + recipient: &{NonFungibleToken.Collection}, + name: String, + description: String, + thumbnail: String, + royaltyReceipient: Address, + ) { + ExampleNFT.totalSupply = ExampleNFT.totalSupply + 1 + self.mintNFTWithId(recipient: recipient, name: name, description: description, thumbnail: thumbnail, royaltyReceipient: royaltyReceipient, id: ExampleNFT.totalSupply) + } + + access(all) fun mint( + name: String, + description: String, + thumbnail: String, + ): @NFT { + ExampleNFT.totalSupply = ExampleNFT.totalSupply + 1 + let newNFT <- create NFT( + id: ExampleNFT.totalSupply, + name: name, + description: description, + thumbnail: thumbnail, + royalties: [] + ) + + return <- newNFT + } + + access(all) fun mintNFTWithId( + recipient: &{NonFungibleToken.Collection}, + name: String, + description: String, + thumbnail: String, + royaltyReceipient: Address, + id: UInt64 + ) { + let royaltyRecipient = getAccount(royaltyReceipient).capabilities.get<&{FungibleToken.Receiver}>(/public/flowTokenReceiver)! + let cutInfo = MetadataViews.Royalty(receiver: royaltyRecipient, cut: 0.05, description: "") + // create a new NFT + var newNFT <- create NFT( + id: id, + name: name, + description: description, + thumbnail: thumbnail, + royalties: [cutInfo] + ) + + // deposit it in the recipient's account using their reference + recipient.deposit(token: <-newNFT) + } + + // mintNFT mints a new NFT with a new ID + // and deposit it in the recipients collection using their collection reference + access(all) fun mintNFTWithRoyaltyCuts( + recipient: &{NonFungibleToken.Collection}, + name: String, + description: String, + thumbnail: String, + royaltyReceipients: [Address], + royaltyCuts: [UFix64] + ) { + assert(royaltyReceipients.length == royaltyCuts.length, message: "mismatched royalty recipients and cuts") + let royalties: [MetadataViews.Royalty] = [] + + var index = 0 + while index < royaltyReceipients.length { + let royaltyRecipient = getAccount(royaltyReceipients[index]).capabilities.get<&{FungibleToken.Receiver}>(/public/flowTokenReceiver)! + let cutInfo = MetadataViews.Royalty(receiver: royaltyRecipient, cut: royaltyCuts[index], description: "") + royalties.append(cutInfo) + index = index + 1 + } + + ExampleNFT.totalSupply = ExampleNFT.totalSupply + 1 + + // create a new NFT + var newNFT <- create NFT( + id: ExampleNFT.totalSupply, + name: name, + description: description, + thumbnail: thumbnail, + royalties: royalties + ) + + // deposit it in the recipient's account using their reference + recipient.deposit(token: <-newNFT) + } + } + + /// Function that resolves a metadata view for this contract. + /// + /// @param view: The Type of the desired view. + /// @return A structure representing the requested view. + /// + access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? { + switch viewType { + case Type(): + return MetadataViews.NFTCollectionData( + storagePath: ExampleNFT.CollectionStoragePath, + publicPath: ExampleNFT.CollectionPublicPath, + publicCollection: Type<&ExampleNFT.Collection>(), + publicLinkedType: Type<&ExampleNFT.Collection>(), + createEmptyCollectionFunction: (fun (): @{NonFungibleToken.Collection} { + return <-ExampleNFT.createEmptyCollection() + }) + ) + case Type(): + let media = MetadataViews.Media( + file: MetadataViews.HTTPFile( + url: "https://assets.website-files.com/5f6294c0c7a8cdd643b1c820/5f6294c0c7a8cda55cb1c936_Flow_Wordmark.svg" + ), + mediaType: "image/svg+xml" + ) + return MetadataViews.NFTCollectionDisplay( + name: "The Example Collection", + description: "This collection is used as an example to help you develop your next Flow NFT.", + externalURL: MetadataViews.ExternalURL("https://example-nft.onflow.org"), + squareImage: media, + bannerImage: media, + socials: { + "twitter": MetadataViews.ExternalURL("https://twitter.com/flow_blockchain") + } + ) + } + return nil + } + + /// Function that returns all the Metadata Views implemented by a Non Fungible Token + /// + /// @return An array of Types defining the implemented views. This value will be used by + /// developers to know which parameter to pass to the resolveView() method. + /// + access(all) view fun getContractViews(resourceType: Type?): [Type] { + return [ + Type(), + Type() + ] + } + + init() { + // Initialize the total supply + self.totalSupply = 0 + + // Set the named paths + self.CollectionStoragePath = /storage/exampleNFTCollection + self.CollectionPublicPath = /public/exampleNFTCollection + self.MinterStoragePath = /storage/exampleNFTMinter + self.MinterPublicPath = /public/exampleNFTMinter + + // Create a Collection resource and save it to storage + let collection <- create Collection() + self.account.storage.save(<-collection, to: self.CollectionStoragePath) + + // create a public capability for the collection + let cap = self.account.capabilities.storage.issue<&ExampleNFT.Collection>(self.CollectionStoragePath) + self.account.capabilities.publish(cap, at: self.CollectionPublicPath) + + // Create a Minter resource and save it to storage + let minter <- create NFTMinter() + self.account.storage.save(<-minter, to: self.MinterStoragePath) + let minterCap = self.account.capabilities.storage.issue<&ExampleNFT.NFTMinter>(self.MinterStoragePath) + self.account.capabilities.publish(minterCap, at: self.MinterPublicPath) + + emit ContractInitialized() + } +} + \ No newline at end of file diff --git a/contracts/example/ExampleToken.cdc b/contracts/example/ExampleToken.cdc new file mode 100644 index 0000000..fc834a4 --- /dev/null +++ b/contracts/example/ExampleToken.cdc @@ -0,0 +1,302 @@ +import "FungibleToken" +import "FungibleTokenMetadataViews" +import "MetadataViews" + + +// THIS CONTRACT IS FOR TESTING PURPOSES ONLY! +access(all) contract ExampleToken { + + /// Total supply of ExampleTokens in existence + access(all) var totalSupply: UFix64 + + /// TokensInitialized + /// + /// The event that is emitted when the contract is created + access(all) event TokensInitialized(initialSupply: UFix64) + + /// TokensWithdrawn + /// + /// The event that is emitted when tokens are withdrawn from a Vault + access(all) event TokensWithdrawn(amount: UFix64, from: Address?) + + /// TokensDeposited + /// + /// The event that is emitted when tokens are deposited to a Vault + access(all) event TokensDeposited(amount: UFix64, to: Address?) + + /// TokensMinted + /// + /// The event that is emitted when new tokens are minted + access(all) event TokensMinted(amount: UFix64) + + /// TokensBurned + /// + /// The event that is emitted when tokens are destroyed + access(all) event TokensBurned(amount: UFix64) + + /// MinterCreated + /// + /// The event that is emitted when a new minter resource is created + access(all) event MinterCreated(allowedAmount: UFix64) + + /// BurnerCreated + /// + /// The event that is emitted when a new burner resource is created + access(all) event BurnerCreated() + + /// Vault + /// + /// Each user stores an instance of only the Vault in their storage + /// The functions in the Vault and governed by the pre and post conditions + /// in FungibleToken when they are called. + /// The checks happen at runtime whenever a function is called. + /// + /// Resources can only be created in the context of the contract that they + /// are defined in, so there is no way for a malicious user to create Vaults + /// out of thin air. A special Minter resource needs to be defined to mint + /// new tokens. + /// + access(all) resource Vault: FungibleToken.Vault { + + /// The total balance of this vault + access(all) var balance: UFix64 + + // initialize the balance at resource creation time + init(balance: UFix64) { + self.balance = balance + } + + access(all) view fun getBalance(): UFix64 { + return self.balance + } + + access(all) view fun getDefaultStoragePath(): StoragePath? { + return /storage/exampleTokenVault + } + + access(all) view fun getDefaultPublicPath(): PublicPath? { + return /public/exampleTokenPublic + } + + access(all) view fun getDefaultReceiverPath(): PublicPath? { + return /public/exampleTokenPublic + } + + access(all) view fun isAvailableToWithdraw(amount: UFix64): Bool { + return self.balance >= amount + } + + /// Same as getViews above, but on a specific NFT instead of a contract + access(all) view fun getViews(): [Type] { + return ExampleToken.getContractViews(resourceType: nil) + } + + /// Same as resolveView above, but on a specific NFT instead of a contract + access(all) fun resolveView(_ view: Type): AnyStruct? { + return ExampleToken.resolveContractView(resourceType: nil, viewType: view) + } + + /// withdraw + /// + /// Function that takes an amount as an argument + /// and withdraws that amount from the Vault. + /// + /// It creates a new temporary Vault that is used to hold + /// the money that is being transferred. It returns the newly + /// created Vault to the context that called so it can be deposited + /// elsewhere. + /// + access(FungibleToken.Withdraw) fun withdraw(amount: UFix64): @{FungibleToken.Vault} { + self.balance = self.balance - amount + emit TokensWithdrawn(amount: amount, from: self.owner?.address) + return <-create Vault(balance: amount) + } + + access(all) view fun getSupportedVaultTypes(): {Type: Bool} { + return { + Type<@ExampleToken.Vault>(): true + } + } + + access(all) view fun isSupportedVaultType(type: Type): Bool { + return type == Type<@ExampleToken.Vault>() + } + + /// deposit + /// + /// Function that takes a Vault object as an argument and adds + /// its balance to the balance of the owners Vault. + /// + /// It is allowed to destroy the sent Vault because the Vault + /// was a temporary holder of the tokens. The Vault's balance has + /// been consumed and therefore can be destroyed. + /// + access(all) fun deposit(from: @{FungibleToken.Vault}) { + let vault <- from as! @ExampleToken.Vault + self.balance = self.balance + vault.balance + emit TokensDeposited(amount: vault.balance, to: self.owner?.address) + vault.balance = 0.0 + destroy vault + } + + access(all) fun createEmptyVault(): @Vault { + return <- ExampleToken.createEmptyVault() + } + } + + /// createEmptyVault + /// + /// Function that creates a new Vault with a balance of zero + /// and returns it to the calling context. A user must call this function + /// and store the returned Vault in their storage in order to allow their + /// account to be able to receive deposits of this token type. + /// + access(all) fun createEmptyVault(): @Vault { + return <-create Vault(balance: 0.0) + } + + access(all) resource Administrator { + + /// createNewMinter + /// + /// Function that creates and returns a new minter resource + /// + access(all) fun createNewMinter(allowedAmount: UFix64): @Minter { + emit MinterCreated(allowedAmount: allowedAmount) + return <-create Minter(allowedAmount: allowedAmount) + } + + /// createNewBurner + /// + /// Function that creates and returns a new burner resource + /// + access(all) fun createNewBurner(): @Burner { + emit BurnerCreated() + return <-create Burner() + } + } + + /// Minter + /// + /// Resource object that token admin accounts can hold to mint new tokens. + /// + access(all) resource Minter { + + /// The amount of tokens that the minter is allowed to mint + access(all) var allowedAmount: UFix64 + + /// mintTokens + /// + /// Function that mints new tokens, adds them to the total supply, + /// and returns them to the calling context. + /// + access(all) fun mintTokens(amount: UFix64): @ExampleToken.Vault { + pre { + amount > 0.0: "Amount minted must be greater than zero" + amount <= self.allowedAmount: "Amount minted must be less than the allowed amount" + } + ExampleToken.totalSupply = ExampleToken.totalSupply + amount + self.allowedAmount = self.allowedAmount - amount + emit TokensMinted(amount: amount) + return <-create Vault(balance: amount) + } + + init(allowedAmount: UFix64) { + self.allowedAmount = allowedAmount + } + } + + /// Burner + /// + /// Resource object that token admin accounts can hold to burn tokens. + /// + access(all) resource Burner { + + /// burnTokens + /// + /// Function that destroys a Vault instance, effectively burning the tokens. + /// + /// Note: the burned tokens are automatically subtracted from the + /// total supply in the Vault destructor. + /// + access(all) fun burnTokens(from: @{FungibleToken.Vault}) { + let vault <- from as! @ExampleToken.Vault + let amount = vault.balance + destroy vault + emit TokensBurned(amount: amount) + } + } + + access(all) view fun getContractViews(resourceType: Type?): [Type] { + return [Type(), + Type(), + Type(), + Type()] + } + + access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? { + switch viewType { + case Type(): + return FungibleTokenMetadataViews.FTView( + ftDisplay: self.resolveContractView(resourceType: nil, viewType: Type()) as! FungibleTokenMetadataViews.FTDisplay?, + ftVaultData: self.resolveContractView(resourceType: nil, viewType: Type()) as! FungibleTokenMetadataViews.FTVaultData? + ) + case Type(): + let media = MetadataViews.Media( + file: MetadataViews.HTTPFile( + url: "https://example.com" + ), + mediaType: "image/svg+xml" + ) + let medias = MetadataViews.Medias([media]) + return FungibleTokenMetadataViews.FTDisplay( + name: "Example Token", + symbol: "EXAMPLE", + description: "", + externalURL: MetadataViews.ExternalURL("https://flow.com"), + logos: medias, + socials: { + "twitter": MetadataViews.ExternalURL("https://twitter.com/flow_blockchain") + } + ) + case Type(): + let vaultRef = ExampleToken.account.storage.borrow(from: /storage/exampleTokenVault) + ?? panic("Could not borrow reference to the contract's Vault!") + return FungibleTokenMetadataViews.FTVaultData( + storagePath: /storage/exampleTokenVault, + receiverPath: /public/exampleTokenReceiver, + metadataPath: /public/exampleTokenBalance, + receiverLinkedType: Type<&{FungibleToken.Receiver, FungibleToken.Vault}>(), + metadataLinkedType: Type<&{FungibleToken.Balance, FungibleToken.Vault}>(), + createEmptyVaultFunction: (fun (): @{FungibleToken.Vault} { + return <-vaultRef.createEmptyVault() + }) + ) + case Type(): + return FungibleTokenMetadataViews.TotalSupply(totalSupply: ExampleToken.totalSupply) + } + return nil + } + + init() { + self.totalSupply = 1000.0 + + // Create the Vault with the total supply of tokens and save it in storage + // + let vault <- create Vault(balance: self.totalSupply) + self.account.storage.save(<-vault, to: /storage/exampleTokenVault) + + // Create a public capability to the stored Vault that only exposes + // the `deposit` method through the `Receiver` interface + // + let publicCap = self.account.capabilities.storage.issue<&ExampleToken.Vault>(/storage/exampleTokenVault) + self.account.capabilities.publish(publicCap, at: /public/exampleTokenPublic) + + let admin <- create Administrator() + self.account.storage.save(<-admin, to: /storage/exampleTokenAdmin) + + // Emit an event that shows that the contract was initialized + // + emit TokensInitialized(initialSupply: self.totalSupply) + } +} \ No newline at end of file diff --git a/contracts/find/FindViews.cdc b/contracts/find/FindViews.cdc index 80386af..98a3962 100644 --- a/contracts/find/FindViews.cdc +++ b/contracts/find/FindViews.cdc @@ -1,67 +1,64 @@ import "NonFungibleToken" import "FungibleToken" import "MetadataViews" +import "ViewResolver" -pub contract FindViews { - - pub struct OnChainFile : MetadataViews.File{ - pub let content: String - pub let mediaType: String - pub let protocol: String - - init(content:String, mediaType: String) { - self.content=content - self.protocol="onChain" - self.mediaType=mediaType - } - - pub fun uri(): String { - return "data:".concat(self.mediaType).concat(",").concat(self.content) - } - } - - pub struct SharedMedia : MetadataViews.File { - pub let mediaType: String - pub let pointer: ViewReadPointer - pub let protocol: String - - init(pointer: ViewReadPointer, mediaType: String) { - self.pointer=pointer - self.mediaType=mediaType - self.protocol="shared" - - if pointer.resolveView(Type()) == nil { - panic("Cannot create shared media if the pointer does not contain StringMedia") - } - } - - pub fun uri(): String { - let media = self.pointer.resolveView(Type()) - if media == nil { - return "" - } - return (media as! OnChainFile).uri() - } - } - - pub resource interface VaultViews { - pub var balance: UFix64 - - pub fun getViews() : [Type] - pub fun resolveView(_ view: Type): AnyStruct? +access(all) contract FindViews { + + access(all) struct OnChainFile : MetadataViews.File{ + access(all) let content: String + access(all) let mediaType: String + access(all) let protocol: String + + init(content:String, mediaType: String) { + self.content=content + self.protocol="onChain" + self.mediaType=mediaType + } + + access(all) view fun uri(): String { + return "data:".concat(self.mediaType).concat(",").concat(self.content) + } } - pub struct FTVaultData { - pub let tokenAlias: String - pub let storagePath: StoragePath - pub let receiverPath: PublicPath - pub let balancePath: PublicPath - pub let providerPath: PrivatePath - pub let vaultType: Type - pub let receiverType: Type - pub let balanceType: Type - pub let providerType: Type - pub let createEmptyVault: ((): @FungibleToken.Vault) + access(all) struct SharedMedia : MetadataViews.File { + access(all) let mediaType: String + access(all) let pointer: ViewReadPointer + access(all) let protocol: String + + init(pointer: ViewReadPointer, mediaType: String) { + self.pointer=pointer + self.mediaType=mediaType + self.protocol="shared" + + if pointer.resolveView(Type()) == nil { + panic("Cannot create shared media if the pointer does not contain StringMedia") + } + } + + // todo: this is not working so we have a workaround in the contract + access(all) view fun uri(): String { + return "data:".concat(self.mediaType).concat(",").concat(self.protocol) + } + } + + access(all) resource interface VaultViews { + access(all) var balance: UFix64 + access(all) view fun getViews() : [Type] + access(all) fun resolveView(_ view: Type): AnyStruct? + } + + access(all) struct FTVaultData { + access(all) let tokenAlias: String + access(all) let storagePath: StoragePath + access(all) let receiverPath: PublicPath + access(all) let balancePath: PublicPath + access(all) let providerPath: PrivatePath + access(all) let vaultType: Type + access(all) let receiverType: Type + access(all) let balanceType: Type + access(all) let providerType: Type + access(all) let createEmptyVault: (fun(): @{FungibleToken.Vault}) init( tokenAlias: String, @@ -73,11 +70,11 @@ pub contract FindViews { receiverType: Type, balanceType: Type, providerType: Type, - createEmptyVault: ((): @FungibleToken.Vault) + createEmptyVault: (fun(): @{FungibleToken.Vault}) ) { pre { receiverType.isSubtype(of: Type<&{FungibleToken.Receiver}>()): "Receiver type must include FungibleToken.Receiver interfaces." - balanceType.isSubtype(of: Type<&{FungibleToken.Balance}>()): "Balance type must include FungibleToken.Balance interfaces." + balanceType.isSubtype(of: Type<&{FungibleToken.Vault}>()): "Balance type must include FungibleToken.Vault interfaces." providerType.isSubtype(of: Type<&{FungibleToken.Provider}>()): "Provider type must include FungibleToken.Provider interface." } self.tokenAlias=tokenAlias @@ -93,300 +90,307 @@ pub contract FindViews { } } - // This is an example taken from Versus - pub struct CreativeWork { - pub let artist: String - pub let name: String - pub let description: String - pub let type: String - - init(artist: String, name: String, description: String, type: String) { - self.artist=artist - self.name=name - self.description=description - self.type=type - } - } - - - /// A basic pointer that can resolve data and get owner/id/uuid and gype - pub struct interface Pointer { - pub let id: UInt64 - pub fun resolveView(_ type: Type) : AnyStruct? - pub fun getUUID() :UInt64 - pub fun getViews() : [Type] - pub fun owner() : Address - pub fun valid() : Bool - pub fun getItemType() : Type - pub fun getViewResolver() : &AnyResource{MetadataViews.Resolver} - - //There are just convenience functions for shared views in the standard - pub fun getRoyalty() : MetadataViews.Royalties - pub fun getTotalRoyaltiesCut() : UFix64 - - //Requred views - pub fun getDisplay() : MetadataViews.Display - pub fun getNFTCollectionData() : MetadataViews.NFTCollectionData - - pub fun checkSoulBound() : Bool - - } - - //An interface to say that this pointer can withdraw - pub struct interface AuthPointer { - pub fun withdraw() : @AnyResource - } - - pub struct ViewReadPointer : Pointer { - access(self) let cap: Capability<&{MetadataViews.ResolverCollection}> - pub let id: UInt64 - pub let uuid: UInt64 - pub let itemType: Type - - init(cap: Capability<&{MetadataViews.ResolverCollection}>, id: UInt64) { - self.cap=cap - self.id=id - - if !self.cap.check() { - panic("The capability is not valid.") - } - let viewResolver=self.cap.borrow()!.borrowViewResolver(id: self.id) - let display = MetadataViews.getDisplay(viewResolver) ?? panic("MetadataViews Display View is not implemented on this NFT.") - let nftCollectionData = MetadataViews.getNFTCollectionData(viewResolver) ?? panic("MetadataViews NFTCollectionData View is not implemented on this NFT.") - self.uuid=viewResolver.uuid - self.itemType=viewResolver.getType() - } - - pub fun resolveView(_ type: Type) : AnyStruct? { - return self.getViewResolver().resolveView(type) - } - - pub fun getUUID() :UInt64{ - return self.uuid - } - - pub fun getViews() : [Type]{ - return self.getViewResolver().getViews() - } - - pub fun owner() : Address { - return self.cap.address - } - - pub fun getTotalRoyaltiesCut() :UFix64 { - var total=0.0 - for royalty in self.getRoyalty().getRoyalties() { - total = total + royalty.cut - } - return total - } - - pub fun getRoyalty() : MetadataViews.Royalties { - if let v = MetadataViews.getRoyalties(self.getViewResolver()) { - return v - } - return MetadataViews.Royalties([]) - } - - pub fun valid() : Bool { - if !self.cap.check() || !self.cap.borrow()!.getIDs().contains(self.id) { - return false - } - return true - } - - pub fun getItemType() : Type { - return self.itemType - } - - pub fun getViewResolver() : &AnyResource{MetadataViews.Resolver} { - return self.cap.borrow()?.borrowViewResolver(id: self.id) ?? panic("The capability of view pointer is not linked.") - } - - pub fun getDisplay() : MetadataViews.Display { - if let v = MetadataViews.getDisplay(self.getViewResolver()) { - return v - } - panic("MetadataViews Display View is not implemented on this NFT.") - } - - pub fun getNFTCollectionData() : MetadataViews.NFTCollectionData { - if let v = MetadataViews.getNFTCollectionData(self.getViewResolver()) { - return v - } - panic("MetadataViews NFTCollectionData View is not implemented on this NFT.") - } - - pub fun checkSoulBound() : Bool { - return FindViews.checkSoulBound(self.getViewResolver()) - } - } - - - pub fun getNounce(_ viewResolver: &{MetadataViews.Resolver}) : UInt64 { - if let nounce = viewResolver.resolveView(Type()) { - if let v = nounce as? FindViews.Nounce { - return v.nounce - } - } - return 0 - } - - - pub struct AuthNFTPointer : Pointer, AuthPointer{ - access(self) let cap: Capability<&{MetadataViews.ResolverCollection, NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}> - pub let id: UInt64 - pub let nounce: UInt64 - pub let uuid: UInt64 - pub let itemType: Type - - init(cap: Capability<&{MetadataViews.ResolverCollection, NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>, id: UInt64) { - self.cap=cap - self.id=id - - if !self.cap.check() { - panic("The capability is not valid.") - } - - let viewResolver=self.cap.borrow()!.borrowViewResolver(id: self.id) - let display = MetadataViews.getDisplay(viewResolver) ?? panic("MetadataViews Display View is not implemented on this NFT.") - let nftCollectionData = MetadataViews.getNFTCollectionData(viewResolver) ?? panic("MetadataViews NFTCollectionData View is not implemented on this NFT.") - self.nounce=FindViews.getNounce(viewResolver) - self.uuid=viewResolver.uuid - self.itemType=viewResolver.getType() - } - - pub fun getViewResolver() : &AnyResource{MetadataViews.Resolver} { - return self.cap.borrow()?.borrowViewResolver(id: self.id) ?? panic("The capability of view pointer is not linked.") - } - - pub fun resolveView(_ type: Type) : AnyStruct? { - return self.getViewResolver().resolveView(type) - } - - pub fun getUUID() :UInt64{ - return self.uuid - } - - pub fun getViews() : [Type]{ - return self.getViewResolver().getViews() - } - - pub fun valid() : Bool { - if !self.cap.check() || !self.cap.borrow()!.getIDs().contains(self.id) { - return false - } - - let viewResolver=self.getViewResolver() - - if let nounce = viewResolver.resolveView(Type()) { - if let v = nounce as? FindViews.Nounce { - return v.nounce==self.nounce - } - } - return true - } - - pub fun getTotalRoyaltiesCut() :UFix64 { - var total=0.0 - for royalty in self.getRoyalty().getRoyalties() { - total = total + royalty.cut - } - return total - } - - pub fun getRoyalty() : MetadataViews.Royalties { - if let v = MetadataViews.getRoyalties(self.getViewResolver()) { - return v - } - return MetadataViews.Royalties([]) - } - - pub fun getDisplay() : MetadataViews.Display { - if let v = MetadataViews.getDisplay(self.getViewResolver()) { - return v - } - panic("MetadataViews Display View is not implemented on this NFT.") - } - - pub fun getNFTCollectionData() : MetadataViews.NFTCollectionData { - if let v = MetadataViews.getNFTCollectionData(self.getViewResolver()) { - return v - } - panic("MetadataViews NFTCollectionData View is not implemented on this NFT.") - } - - pub fun withdraw() :@NonFungibleToken.NFT { - if !self.cap.check() { - panic("The pointer capability is invalid.") - } - return <- self.cap.borrow()!.withdraw(withdrawID: self.id) - } - - pub fun deposit(_ nft: @NonFungibleToken.NFT){ + // This is an example taken from Versus + access(all) struct CreativeWork { + access(all) let artist: String + access(all) let name: String + access(all) let description: String + access(all) let type: String + + init(artist: String, name: String, description: String, type: String) { + self.artist=artist + self.name=name + self.description=description + self.type=type + } + } + + /// A basic pointer that can resolve data and get owner/id/uuid and gype + access(all) struct interface Pointer { + access(all) let id: UInt64 + access(all) fun resolveView(_ type: Type) : AnyStruct? + access(all) fun getUUID() :UInt64 + access(all) fun getViews() : [Type] + access(all) fun owner() : Address + access(all) fun valid() : Bool + access(all) fun getItemType() : Type + access(all) fun getViewResolver() : &{ViewResolver.Resolver} + + //There are just convenience functions for shared views in the standard + access(all) fun getRoyalty() : MetadataViews.Royalties + access(all) fun getTotalRoyaltiesCut() : UFix64 + + //Requred views + access(all) fun getDisplay() : MetadataViews.Display + access(all) fun getNFTCollectionData() : MetadataViews.NFTCollectionData + + access(all) fun checkSoulBound() : Bool + + } + + //An interface to say that this pointer can withdraw + access(all) struct interface AuthPointer { + access(all) fun withdraw() : @AnyResource + } + + access(all) struct ViewReadPointer : Pointer { + access(self) let cap: Capability<&{ViewResolver.ResolverCollection}> + access(all) let id: UInt64 + access(all) let uuid: UInt64 + access(all) let itemType: Type + + init(cap: Capability<&{ViewResolver.ResolverCollection}>, id: UInt64) { + self.cap=cap + self.id=id + + if !self.cap.check() { + panic("The capability is not valid.") + } + let viewResolver=self.cap.borrow()!.borrowViewResolver(id: self.id)! + let display = MetadataViews.getDisplay(viewResolver) ?? panic("MetadataViews Display View is not implemented on this NFT.") + let nftCollectionData = MetadataViews.getNFTCollectionData(viewResolver) ?? panic("MetadataViews NFTCollectionData View is not implemented on this NFT.") + self.uuid=viewResolver.uuid + self.itemType=viewResolver.getType() + } + + access(all) fun resolveView(_ type: Type) : AnyStruct? { + return self.getViewResolver().resolveView(type) + } + + access(all) fun getUUID() :UInt64{ + return self.uuid + } + + access(all) fun getViews() : [Type]{ + return self.getViewResolver().getViews() + } + + access(all) fun owner() : Address { + return self.cap.address + } + + access(all) fun getTotalRoyaltiesCut() :UFix64 { + var total=0.0 + for royalty in self.getRoyalty().getRoyalties() { + total = total + royalty.cut + } + return total + } + + access(all) fun getRoyalty() : MetadataViews.Royalties { + if let v = MetadataViews.getRoyalties(self.getViewResolver()) { + return v + } + return MetadataViews.Royalties([]) + } + + access(all) fun valid() : Bool { + if !self.cap.check() || self.cap.borrow()!.borrowViewResolver(id: self.id) == nil { + return false + } + return true + } + + access(all) fun getItemType() : Type { + return self.itemType + } + + access(all) fun getViewResolver() : &{ViewResolver.Resolver} { + let nft=self.cap.borrow()!.borrowViewResolver(id: self.id) ?? panic("The capability of view pointer is not linked.") + return nft + + } + + access(all) fun getDisplay() : MetadataViews.Display { + if let v = MetadataViews.getDisplay(self.getViewResolver()) { + return v + } + panic("MetadataViews Display View is not implemented on this NFT.") + } + + access(all) fun getNFTCollectionData() : MetadataViews.NFTCollectionData { + if let v = MetadataViews.getNFTCollectionData(self.getViewResolver()) { + return v + } + panic("MetadataViews NFTCollectionData View is not implemented on this NFT.") + } + + access(all) fun checkSoulBound() : Bool { + return FindViews.checkSoulBound(self.getViewResolver()) + } + } + + + access(all) fun getNounce(_ viewResolver: &{ViewResolver.Resolver}) : UInt64 { + if let nounce = viewResolver.resolveView(Type()) { + if let v = nounce as? FindViews.Nounce { + return v.nounce + } + } + return 0 + } + + access(all) struct AuthNFTPointer : Pointer, AuthPointer{ + access(self) let cap: Capability + access(all) let id: UInt64 + access(all) let nounce: UInt64 + access(all) let uuid: UInt64 + access(all) let itemType: Type + + init(cap: Capability, id: UInt64) { + self.cap=cap + self.id=id + + if !self.cap.check() { + panic("The capability is not valid.") + } + + let collection = self.cap.borrow() ?? panic("could not find collection") + let viewResolver=collection.borrowNFT(self.id) ?? panic("could not borrow nft") + let display = MetadataViews.getDisplay(viewResolver) ?? panic("MetadataViews Display View is not implemented on this NFT.") + let nftCollectionData = MetadataViews.getNFTCollectionData(viewResolver) ?? panic("MetadataViews NFTCollectionData View is not implemented on this NFT.") + self.nounce=FindViews.getNounce(viewResolver) + self.uuid=viewResolver.uuid + self.itemType=viewResolver.getType() + } + + access(all) fun getViewResolver() : &{ViewResolver.Resolver} { + let cap = self.cap.borrow()! + let viewResolver = cap.borrowNFT(self.id) ?? panic("The capability of view pointer is not linked.") + return viewResolver + } + + access(all) fun resolveView(_ type: Type) : AnyStruct? { + return self.getViewResolver().resolveView(type) + } + + access(all) fun getUUID() :UInt64{ + return self.uuid + } + + access(all) fun getViews() : [Type]{ + return self.getViewResolver().getViews() + } + + access(all) fun valid() : Bool { + if !self.cap.check() { + return false + } + + let nft= self.cap.borrow()!.borrowNFT(self.id) + + if nft ==nil { + return false + } + + if let nounce = nft!.resolveView(Type()) { + if let v = nounce as? FindViews.Nounce { + return v.nounce==self.nounce + } + } + return true + } + + access(all) fun getTotalRoyaltiesCut() :UFix64 { + var total=0.0 + for royalty in self.getRoyalty().getRoyalties() { + total = total + royalty.cut + } + return total + } + + access(all) fun getRoyalty() : MetadataViews.Royalties { + if let v = MetadataViews.getRoyalties(self.getViewResolver()) { + return v + } + return MetadataViews.Royalties([]) + } + + access(all) fun getDisplay() : MetadataViews.Display { + if let v = MetadataViews.getDisplay(self.getViewResolver()) { + return v + } + panic("MetadataViews Display View is not implemented on this NFT.") + } + + access(all) fun getNFTCollectionData() : MetadataViews.NFTCollectionData { + if let v = MetadataViews.getNFTCollectionData(self.getViewResolver()) { + return v + } + panic("MetadataViews NFTCollectionData View is not implemented on this NFT.") + } + + access(all) fun withdraw() :@{NonFungibleToken.NFT} { + if !self.cap.check() { + panic("The pointer capability is invalid.") + } + return <- self.cap.borrow()!.withdraw(withdrawID: self.id) + } + + access(all) fun deposit(_ nft: @{NonFungibleToken.NFT}){ if !self.cap.check(){ panic("The pointer capablity is invalid.") } - self.cap.borrow()!.deposit(token: <- nft) - } - - pub fun owner() : Address { - return self.cap.address - } - - pub fun getItemType() : Type { - return self.itemType - } - - pub fun checkSoulBound() : Bool { - return FindViews.checkSoulBound(self.getViewResolver()) - } - } - - pub fun createViewReadPointer(address:Address, path:PublicPath, id:UInt64) : ViewReadPointer { - let cap= getAccount(address).getCapability<&{MetadataViews.ResolverCollection}>(path) - let pointer= FindViews.ViewReadPointer(cap: cap, id: id) - return pointer - } - - pub struct Nounce { - pub let nounce: UInt64 - - init(_ nounce: UInt64) { - self.nounce=nounce - } - } - - pub struct SoulBound { - - pub let message: String - - init(_ message:String) { - self.message=message - - } - } - - pub fun checkSoulBound(_ viewResolver: &{MetadataViews.Resolver}) : Bool { - if let soulBound = viewResolver.resolveView(Type()) { - if let v = soulBound as? FindViews.SoulBound { - return true - } - } - return false - } - - pub fun getDapperAddress(): Address { - switch FindViews.account.address.toString() { - case "0x097bafa4e0b48eef": - //mainnet - return 0xead892083b3e2c6c - case "0x35717efbbce11c74": - //testnet - return 0x82ec283f88a62e65 - default: - //emulator - return 0x01cf0e2f2f715450 - } - } + self.cap.borrow()!.deposit(token: <- nft) + } + + access(all) fun owner() : Address { + return self.cap.address + } + + access(all) fun getItemType() : Type { + return self.itemType + } + + access(all) fun checkSoulBound() : Bool { + return FindViews.checkSoulBound(self.getViewResolver()) + } + } + + access(all) fun createViewReadPointer(address:Address, path:PublicPath, id:UInt64) : ViewReadPointer { + let cap= getAccount(address).capabilities.get<&{ViewResolver.ResolverCollection}>(path)! + let pointer= FindViews.ViewReadPointer(cap: cap, id: id) + return pointer + } + + access(all) struct Nounce { + access(all) let nounce: UInt64 + + init(_ nounce: UInt64) { + self.nounce=nounce + } + } + + access(all) struct SoulBound { + + access(all) let message: String + + init(_ message:String) { + self.message=message + + } + } + + access(all) fun checkSoulBound(_ viewResolver: &{ViewResolver.Resolver}) : Bool { + if let soulBound = viewResolver.resolveView(Type()) { + if let v = soulBound as? FindViews.SoulBound { + return true + } + } + return false + } + + access(all) fun getDapperAddress(): Address { + switch FindViews.account.address.toString() { + case "0x097bafa4e0b48eef": + //mainnet + return 0xead892083b3e2c6c + case "0x35717efbbce11c74": + //testnet + return 0x82ec283f88a62e65 + default: + //emulator + return 0x01cf0e2f2f715450 + } + } } \ No newline at end of file diff --git a/contracts/flow-utils/AddressUtils.cdc b/contracts/flow-utils/AddressUtils.cdc index bd61db5..39dfe45 100644 --- a/contracts/flow-utils/AddressUtils.cdc +++ b/contracts/flow-utils/AddressUtils.cdc @@ -1,8 +1,5 @@ -import "StringUtils" - -pub contract AddressUtils { - - pub fun withoutPrefix(_ input: String): String { +access(all) contract AddressUtils { + access(all) fun withoutPrefix(_ input: String): String { var address = input // get rid of 0x @@ -17,7 +14,7 @@ pub contract AddressUtils { return address } - pub fun parseUInt64(_ input: AnyStruct): UInt64? { + access(all) fun parseUInt64(_ input: AnyStruct): UInt64? { var stringValue = "" if let string = input as? String { @@ -25,7 +22,7 @@ pub contract AddressUtils { } else if let address = input as? Address { stringValue = address.toString() } else if let type = input as? Type { - let parts = StringUtils.split(type.identifier, ".") + let parts = type.identifier.split(separator: ".") if parts.length == 1 { return nil } @@ -45,14 +42,14 @@ pub contract AddressUtils { return r } - pub fun parseAddress(_ input: AnyStruct): Address? { + access(all) fun parseAddress(_ input: AnyStruct): Address? { if let parsed = self.parseUInt64(input) { return Address(parsed) } return nil } - pub fun isValidAddress(_ input: AnyStruct, forNetwork: String): Bool { + access(all) fun isValidAddress(_ input: AnyStruct, forNetwork: String): Bool { let address = self.parseUInt64(input) if address == nil { return false @@ -61,18 +58,19 @@ pub contract AddressUtils { let codeWords: {String: UInt64} = { "MAINNET" : 0, "TESTNET" : 0x6834ba37b3980209, + "CRESCENDO" : 0x6834ba37b3980209, "EMULATOR": 0x1cb159857af02018 } let parityCheckMatrixColumns: [UInt64] = [ - 0x00001, 0x00002, 0x00004, 0x00008, 0x00010, 0x00020, 0x00040, 0x00080, - 0x00100, 0x00200, 0x00400, 0x00800, 0x01000, 0x02000, 0x04000, 0x08000, - 0x10000, 0x20000, 0x40000, 0x7328d, 0x6689a, 0x6112f, 0x6084b, 0x433fd, - 0x42aab, 0x41951, 0x233ce, 0x22a81, 0x21948, 0x1ef60, 0x1deca, 0x1c639, - 0x1bdd8, 0x1a535, 0x194ac, 0x18c46, 0x1632b, 0x1529b, 0x14a43, 0x13184, - 0x12942, 0x118c1, 0x0f812, 0x0e027, 0x0d00e, 0x0c83c, 0x0b01d, 0x0a831, - 0x0982b, 0x07034, 0x0682a, 0x05819, 0x03807, 0x007d2, 0x00727, 0x0068e, - 0x0067c, 0x0059d, 0x004eb, 0x003b4, 0x0036a, 0x002d9, 0x001c7, 0x0003f + 0x00001, 0x00002, 0x00004, 0x00008, 0x00010, 0x00020, 0x00040, 0x00080, + 0x00100, 0x00200, 0x00400, 0x00800, 0x01000, 0x02000, 0x04000, 0x08000, + 0x10000, 0x20000, 0x40000, 0x7328d, 0x6689a, 0x6112f, 0x6084b, 0x433fd, + 0x42aab, 0x41951, 0x233ce, 0x22a81, 0x21948, 0x1ef60, 0x1deca, 0x1c639, + 0x1bdd8, 0x1a535, 0x194ac, 0x18c46, 0x1632b, 0x1529b, 0x14a43, 0x13184, + 0x12942, 0x118c1, 0x0f812, 0x0e027, 0x0d00e, 0x0c83c, 0x0b01d, 0x0a831, + 0x0982b, 0x07034, 0x0682a, 0x05819, 0x03807, 0x007d2, 0x00727, 0x0068e, + 0x0067c, 0x0059d, 0x004eb, 0x003b4, 0x0036a, 0x002d9, 0x001c7, 0x0003f ] var parity: UInt64 = 0 @@ -93,8 +91,8 @@ pub contract AddressUtils { return parity == 0 && codeWord == 0 } - pub fun getNetworkFromAddress(_ input: AnyStruct): String? { - for network in ["MAINNET", "TESTNET", "EMULATOR"]{ + access(all) fun getNetworkFromAddress(_ input: AnyStruct): String? { + for network in ["MAINNET", "TESTNET", "EMULATOR"] { if self.isValidAddress(input, forNetwork: network){ return network } @@ -102,8 +100,7 @@ pub contract AddressUtils { return nil } - pub fun currentNetwork(): String { - return self.getNetworkFromAddress(self.account.address)! + access(all) fun currentNetwork(): String { + return self.getNetworkFromAddress(self.account.address) ?? panic("unknown network!") } - -} \ No newline at end of file +} diff --git a/contracts/flow-utils/ArrayUtils.cdc b/contracts/flow-utils/ArrayUtils.cdc index c9565a0..7996fdd 100644 --- a/contracts/flow-utils/ArrayUtils.cdc +++ b/contracts/flow-utils/ArrayUtils.cdc @@ -1,8 +1,7 @@ // Copied from https://github.com/bluesign/flow-utils/blob/dnz/cadence/contracts/ArrayUtils.cdc with minor adjustments -pub contract ArrayUtils { - - pub fun rangeFunc(_ start: Int, _ end: Int, _ f: ((Int): Void)) { +access(all) contract ArrayUtils { + access(all) fun rangeFunc(_ start: Int, _ end: Int, _ f: fun (Int): Void) { var current = start while current < end { f(current) @@ -10,7 +9,7 @@ pub contract ArrayUtils { } } - pub fun range(_ start: Int, _ end: Int): [Int] { + access(all) fun range(_ start: Int, _ end: Int): [Int] { var res: [Int] = [] self.rangeFunc(start, end, fun (i: Int) { res.append(i) @@ -18,7 +17,7 @@ pub contract ArrayUtils { return res } - pub fun reverse(_ array: [Int]): [Int] { + access(all) fun reverse(_ array: [Int]): [Int] { var res: [Int] = [] var i: Int = array.length - 1 while i >= 0 { @@ -28,13 +27,13 @@ pub contract ArrayUtils { return res } - pub fun transform(_ array: &[AnyStruct], _ f : ((AnyStruct): AnyStruct)){ + access(all) fun transform(_ array: auth(Mutate) &[AnyStruct], _ f : fun (&AnyStruct, auth(Mutate) &[AnyStruct], Int)){ for i in self.range(0, array.length){ - array[i] = f(array[i]) + f(array[i], array, i) } } - pub fun iterate(_ array: [AnyStruct], _ f : ((AnyStruct): Bool)){ + access(all) fun iterate(_ array: [AnyStruct], _ f : fun (AnyStruct): Bool) { for item in array{ if !f(item){ break @@ -42,7 +41,7 @@ pub contract ArrayUtils { } } - pub fun map(_ array: [AnyStruct], _ f : ((AnyStruct): AnyStruct)) : [AnyStruct] { + access(all) fun map(_ array: [AnyStruct], _ f : fun (AnyStruct): AnyStruct) : [AnyStruct] { var res : [AnyStruct] = [] for item in array{ res.append(f(item)) @@ -50,7 +49,7 @@ pub contract ArrayUtils { return res } - pub fun mapStrings(_ array: [String], _ f: ((String) : String) ) : [String] { + access(all) fun mapStrings(_ array: [String], _ f: fun (String) : String) : [String] { var res : [String] = [] for item in array{ res.append(f(item)) @@ -58,7 +57,7 @@ pub contract ArrayUtils { return res } - pub fun reduce(_ array: [AnyStruct], _ initial: AnyStruct, _ f : ((AnyStruct, AnyStruct): AnyStruct)) : AnyStruct{ + access(all) fun reduce(_ array: [AnyStruct], _ initial: AnyStruct, _ f : fun (AnyStruct, AnyStruct): AnyStruct) : AnyStruct{ var res: AnyStruct = f(initial, array[0]) for i in self.range(1, array.length){ res = f(res, array[i]) diff --git a/contracts/flow-utils/ScopedFTProviders.cdc b/contracts/flow-utils/ScopedFTProviders.cdc index e2ef3dc..3642ee3 100644 --- a/contracts/flow-utils/ScopedFTProviders.cdc +++ b/contracts/flow-utils/ScopedFTProviders.cdc @@ -8,14 +8,14 @@ import "StringUtils" // // ScopedProviders are meant to solve the issue of unbounded access FungibleToken vaults // when a provider is called for. -pub contract ScopedFTProviders { - pub struct interface FTFilter { - pub fun canWithdrawAmount(_ amount: UFix64): Bool - pub fun markAmountWithdrawn(_ amount: UFix64) - pub fun getDetails(): {String: AnyStruct} +access(all) contract ScopedFTProviders { + access(all) struct interface FTFilter { + access(all) view fun canWithdrawAmount(_ amount: UFix64): Bool + access(FungibleToken.Withdraw) fun markAmountWithdrawn(_ amount: UFix64) + access(all) fun getDetails(): {String: AnyStruct} } - pub struct AllowanceFilter: FTFilter { + access(all) struct AllowanceFilter: FTFilter { access(self) let allowance: UFix64 access(self) var allowanceUsed: UFix64 @@ -24,15 +24,15 @@ pub contract ScopedFTProviders { self.allowanceUsed = 0.0 } - pub fun canWithdrawAmount(_ amount: UFix64): Bool { + access(all) view fun canWithdrawAmount(_ amount: UFix64): Bool { return amount + self.allowanceUsed <= self.allowance } - pub fun markAmountWithdrawn(_ amount: UFix64) { + access(FungibleToken.Withdraw) fun markAmountWithdrawn(_ amount: UFix64) { self.allowanceUsed = self.allowanceUsed + amount } - pub fun getDetails(): {String: AnyStruct} { + access(all) fun getDetails(): {String: AnyStruct} { return { "allowance": self.allowance, "allowanceUsed": self.allowanceUsed @@ -44,31 +44,35 @@ pub contract ScopedFTProviders { // // A ScopedFTProvider is a wrapped FungibleTokenProvider with // filters that can be defined by anyone using the ScopedFTProvider. - pub resource ScopedFTProvider: FungibleToken.Provider { - access(self) let provider: Capability<&{FungibleToken.Provider}> + access(all) resource ScopedFTProvider: FungibleToken.Provider { + access(self) let provider: Capability access(self) var filters: [{FTFilter}] // block timestamp that this provider can no longer be used after access(self) let expiration: UFix64? - pub init(provider: Capability<&{FungibleToken.Provider}>, filters: [{FTFilter}], expiration: UFix64?) { + access(all) init(provider: Capability, filters: [{FTFilter}], expiration: UFix64?) { self.provider = provider self.filters = filters self.expiration = expiration } - pub fun check(): Bool { + access(all) fun getProviderType(): Type { + return self.provider.borrow()!.getType() + } + + access(all) fun check(): Bool { return self.provider.check() } - pub fun isExpired(): Bool { + access(all) view fun isExpired(): Bool { if let expiration = self.expiration { return getCurrentBlock().timestamp >= expiration } return false } - pub fun canWithdraw(_ amount: UFix64): Bool { + access(all) view fun canWithdraw(_ amount: UFix64): Bool { if self.isExpired() { return false } @@ -82,7 +86,11 @@ pub contract ScopedFTProviders { return true } - pub fun withdraw(amount: UFix64): @FungibleToken.Vault { + access(all) view fun isAvailableToWithdraw(amount: UFix64): Bool { + return self.canWithdraw(amount) + } + + access(FungibleToken.Withdraw) fun withdraw(amount: UFix64): @{FungibleToken.Vault} { pre { !self.isExpired(): "provider has expired" } @@ -100,7 +108,7 @@ pub contract ScopedFTProviders { return <-self.provider.borrow()!.withdraw(amount: amount) } - pub fun getDetails(): [{String: AnyStruct}] { + access(all) fun getDetails(): [{String: AnyStruct}] { let details: [{String: AnyStruct}] = [] for filter in self.filters { details.append(filter.getDetails()) @@ -110,8 +118,8 @@ pub contract ScopedFTProviders { } } - pub fun createScopedFTProvider( - provider: Capability<&{FungibleToken.Provider}>, + access(all) fun createScopedFTProvider( + provider: Capability, filters: [{FTFilter}], expiration: UFix64? ): @ScopedFTProvider { diff --git a/contracts/flow-utils/ScopedNFTProviders.cdc b/contracts/flow-utils/ScopedNFTProviders.cdc index 83bb6db..a3a4661 100644 --- a/contracts/flow-utils/ScopedNFTProviders.cdc +++ b/contracts/flow-utils/ScopedNFTProviders.cdc @@ -11,14 +11,14 @@ import "StringUtils" // // By using a scoped provider, only a subset of assets can be taken if the provider leaks // instead of the entire nft collection. -pub contract ScopedNFTProviders { - pub struct interface NFTFilter { - pub fun canWithdraw(_ nft: &NonFungibleToken.NFT): Bool - pub fun markWithdrawn(_ nft: &NonFungibleToken.NFT) - pub fun getDetails(): {String: AnyStruct} +access(all) contract ScopedNFTProviders { + access(all) struct interface NFTFilter { + access(all) fun canWithdraw(_ nft: &{NonFungibleToken.NFT}): Bool + access(NonFungibleToken.Withdraw) fun markWithdrawn(_ nft: &{NonFungibleToken.NFT}) + access(all) fun getDetails(): {String: AnyStruct} } - pub struct NFTIDFilter: NFTFilter { + access(all) struct NFTIDFilter: NFTFilter { // the ids that are allowed to be withdrawn. // If ids[num] is false, the id cannot be withdrawn anymore access(self) let ids: {UInt64: Bool} @@ -31,22 +31,22 @@ pub contract ScopedNFTProviders { self.ids = d } - pub fun canWithdraw(_ nft: &NonFungibleToken.NFT): Bool { + access(all) fun canWithdraw(_ nft: &{NonFungibleToken.NFT}): Bool { return self.ids[nft.id] != nil && self.ids[nft.id] == true } - pub fun markWithdrawn(_ nft: &NonFungibleToken.NFT) { + access(NonFungibleToken.Withdraw) fun markWithdrawn(_ nft: &{NonFungibleToken.NFT}) { self.ids[nft.id] = false } - pub fun getDetails(): {String: AnyStruct} { + access(all) fun getDetails(): {String: AnyStruct} { return { "ids": self.ids } } } - pub struct UUIDFilter: NFTFilter { + access(all) struct UUIDFilter: NFTFilter { // the ids that are allowed to be withdrawn. // If ids[num] is false, the id cannot be withdrawn anymore access(self) let uuids: {UInt64: Bool} @@ -59,15 +59,15 @@ pub contract ScopedNFTProviders { self.uuids = d } - pub fun canWithdraw(_ nft: &NonFungibleToken.NFT): Bool { + access(all) fun canWithdraw(_ nft: &{NonFungibleToken.NFT}): Bool { return self.uuids[nft.uuid] != nil && self.uuids[nft.uuid]! == true } - pub fun markWithdrawn(_ nft: &NonFungibleToken.NFT) { + access(NonFungibleToken.Withdraw) fun markWithdrawn(_ nft: &{NonFungibleToken.NFT}) { self.uuids[nft.uuid] = false } - pub fun getDetails(): {String: AnyStruct} { + access(all) fun getDetails(): {String: AnyStruct} { return { "uuids": self.uuids } @@ -77,39 +77,43 @@ pub contract ScopedNFTProviders { // ScopedNFTProvider // // Wrapper around an NFT Provider that is restricted to specific ids. - pub resource ScopedNFTProvider: NonFungibleToken.Provider { - access(self) let provider: Capability<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}> + access(all) resource ScopedNFTProvider: NonFungibleToken.Provider { + access(self) let provider: Capability access(self) let filters: [{NFTFilter}] // block timestamp that this provider can no longer be used after access(self) let expiration: UFix64? - pub fun isExpired(): Bool { + access(all) view fun isExpired(): Bool { if let expiration = self.expiration { return getCurrentBlock().timestamp >= expiration } return false } - pub init(provider: Capability<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>, filters: [{NFTFilter}], expiration: UFix64?) { + access(all) init(provider: Capability, filters: [{NFTFilter}], expiration: UFix64?) { self.provider = provider self.expiration = expiration self.filters = filters } - pub fun canWithdraw(_ id: UInt64): Bool { + access(all) fun canWithdraw(_ id: UInt64): Bool { if self.isExpired() { return false } - let nft = self.provider.borrow()!.borrowNFT(id: id) + if !self.provider.check() { + return false + } + + let nft: &{NonFungibleToken.NFT}? = self.provider.borrow()!.borrowNFT(id) if nft == nil { return false } var i = 0 while i < self.filters.length { - if !self.filters[i].canWithdraw(nft) { + if !self.filters[i].canWithdraw(nft!) { return false } i = i + 1 @@ -117,17 +121,17 @@ pub contract ScopedNFTProviders { return true } - pub fun check(): Bool { + access(all) fun check(): Bool { return self.provider.check() } - pub fun withdraw(withdrawID: UInt64): @NonFungibleToken.NFT { + access(NonFungibleToken.Withdraw) fun withdraw(withdrawID: UInt64): @{NonFungibleToken.NFT} { pre { !self.isExpired(): "provider has expired" } let nft <- self.provider.borrow()!.withdraw(withdrawID: withdrawID) - let ref = &nft as &NonFungibleToken.NFT + let ref = &nft as &{NonFungibleToken.NFT} var i = 0 while i < self.filters.length { @@ -142,7 +146,7 @@ pub contract ScopedNFTProviders { return <-nft } - pub fun getDetails(): [{String: AnyStruct}] { + access(all) fun getDetails(): [{String: AnyStruct}] { let details: [{String: AnyStruct}] = [] for f in self.filters { details.append(f.getDetails()) @@ -152,11 +156,12 @@ pub contract ScopedNFTProviders { } } - pub fun createScopedNFTProvider( - provider: Capability<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>, + access(all) fun createScopedNFTProvider( + provider: Capability, filters: [{NFTFilter}], expiration: UFix64? ): @ScopedNFTProvider { return <- create ScopedNFTProvider(provider: provider, filters: filters, expiration: expiration) } } + diff --git a/contracts/flow-utils/StringUtils.cdc b/contracts/flow-utils/StringUtils.cdc index 34b0e7d..f9f2ba6 100644 --- a/contracts/flow-utils/StringUtils.cdc +++ b/contracts/flow-utils/StringUtils.cdc @@ -1,16 +1,16 @@ import "ArrayUtils" -pub contract StringUtils { - - pub fun format(_ s: String, _ args: {String:String}): String{ - var formatted = s - for key in args.keys{ - formatted = StringUtils.replaceAll(formatted, "{".concat(key).concat("}"), args[key]!) - } - return formatted +access(all) contract StringUtils { + + access(all) fun format(_ s: String, _ args: {String:String}): String{ + var formatted = s + for key in args.keys{ + formatted = StringUtils.replaceAll(formatted, "{".concat(key).concat("}"), args[key]!) + } + return formatted } - pub fun explode(_ s: String): [String]{ + access(all) fun explode(_ s: String): [String]{ var chars : [String] = [] for i in ArrayUtils.range(0, s.length){ chars.append(s[i].toString()) @@ -18,7 +18,7 @@ pub contract StringUtils { return chars } - pub fun trimLeft(_ s: String): String{ + access(all) fun trimLeft(_ s: String): String{ for i in ArrayUtils.range(0, s.length){ if s[i] != " "{ return s.slice(from: i, upTo: s.length) @@ -27,23 +27,23 @@ pub contract StringUtils { return "" } - pub fun trim(_ s: String): String{ + access(all) fun trim(_ s: String): String{ return self.trimLeft(s) } - pub fun replaceAll(_ s: String, _ search: String, _ replace: String): String{ - return self.join(self.split(s, search), replace) + access(all) fun replaceAll(_ s: String, _ search: String, _ replace: String): String{ + return s.replaceAll(of: search, with: replace) } - pub fun hasPrefix(_ s: String, _ prefix: String) : Bool{ + access(all) fun hasPrefix(_ s: String, _ prefix: String) : Bool{ return s.length >= prefix.length && s.slice(from:0, upTo: prefix.length)==prefix } - pub fun hasSuffix(_ s: String, _ suffix: String) : Bool{ + access(all) fun hasSuffix(_ s: String, _ suffix: String) : Bool{ return s.length >= suffix.length && s.slice(from:s.length-suffix.length, upTo: s.length)==suffix } - pub fun index(_ s : String, _ substr : String, _ startIndex: Int): Int?{ + access(all) fun index(_ s : String, _ substr : String, _ startIndex: Int): Int?{ for i in ArrayUtils.range(startIndex,s.length-substr.length+1){ if s[i]==substr[0] && s.slice(from:i, upTo:i+substr.length) == substr{ return i @@ -52,7 +52,7 @@ pub contract StringUtils { return nil } - pub fun count(_ s: String, _ substr: String): Int{ + access(all) fun count(_ s: String, _ substr: String): Int{ var pos = [self.index(s, substr, 0)] while pos[0]!=nil { pos.insert(at:0, self.index(s, substr, pos[0]!+pos.length*substr.length+1)) @@ -60,38 +60,25 @@ pub contract StringUtils { return pos.length-1 } - pub fun contains(_ s: String, _ substr: String): Bool { + access(all) fun contains(_ s: String, _ substr: String): Bool { if let index = self.index(s, substr, 0) { return true } return false } - pub fun substringUntil(_ s: String, _ until: String, _ startIndex: Int): String{ + access(all) fun substringUntil(_ s: String, _ until: String, _ startIndex: Int): String{ if let index = self.index( s, until, startIndex){ return s.slice(from:startIndex, upTo: index) } return s.slice(from:startIndex, upTo:s.length) } - pub fun split(_ s: String, _ delimiter: String): [String] { - let segments: [String] = [] - var p = 0 - while p<=s.length{ - var preDelimiter = self.substringUntil(s, delimiter, p) - segments.append(preDelimiter) - p = p + preDelimiter.length + delimiter.length - } - return segments + access(all) fun split(_ s: String, _ delimiter: String): [String] { + return s.split(separator: delimiter) } - pub fun join(_ strs: [String], _ separator: String): String { - var joinedStr = "" - for s in strs { - joinedStr = joinedStr.concat(s).concat(separator) - } - return joinedStr.slice(from: 0, upTo: joinedStr.length - separator.length) + access(all) fun join(_ strs: [String], _ separator: String): String { + return String.join(strs, separator: separator) } - - -} \ No newline at end of file +} diff --git a/contracts/flowty-drops/ContractManager.cdc b/contracts/flowty-drops/ContractManager.cdc new file mode 100644 index 0000000..3ddf351 --- /dev/null +++ b/contracts/flowty-drops/ContractManager.cdc @@ -0,0 +1,73 @@ +import "FlowToken" +import "FungibleToken" +import "FungibleTokenRouter" + +access(all) contract ContractManager { + access(all) let StoragePath: StoragePath + access(all) let PublicPath: PublicPath + + access(all) entitlement Manage + + access(all) resource Manager { + access(self) let acct: Capability + access(self) let routerCap: Capability + + access(all) let data: {String: AnyStruct} + access(all) let resources: @{String: AnyResource} + + access(Manage) fun borrowContractAccount(): auth(Contracts) &Account { + return self.acct.borrow()! + } + + access(Manage) fun addOverride(type: Type, addr: Address) { + let router = self.routerCap.borrow() ?? panic("fungible token router is not valid") + router.addOverride(type: type, addr: addr) + } + + access(Manage) fun getSwitchboard(): auth(FungibleTokenRouter.Owner) &FungibleTokenRouter.Router { + return self.routerCap.borrow()! + } + + access(all) fun addFlowTokensToAccount(_ tokens: @FlowToken.Vault) { + self.acct.borrow()!.storage.borrow<&{FungibleToken.Receiver}>(from: /storage/flowTokenVault)!.deposit(from: <-tokens) + } + + access(all) fun getAccount(): &Account { + return getAccount(self.acct.address) + } + + init(tokens: @FlowToken.Vault, defaultRouterAddress: Address) { + pre { + tokens.balance >= 0.001: "minimum balance of 0.001 required for initialization" + } + + let acct = Account(payer: ContractManager.account) + self.acct = acct.capabilities.account.issue() + assert(self.acct.check(), message: "failed to setup account capability") + + acct.storage.borrow<&{FungibleToken.Receiver}>(from: /storage/flowTokenVault)!.deposit(from: <-tokens) + + let router <- FungibleTokenRouter.createRouter(defaultAddress: defaultRouterAddress) + acct.storage.save(<-router, to: FungibleTokenRouter.StoragePath) + + let receiver = acct.capabilities.storage.issue<&{FungibleToken.Receiver}>(FungibleTokenRouter.StoragePath) + assert(receiver.check(), message: "invalid switchboard receiver capability") + acct.capabilities.publish(receiver, at: FungibleTokenRouter.PublicPath) + + self.routerCap = acct.capabilities.storage.issue(FungibleTokenRouter.StoragePath) + + self.data = {} + self.resources <- {} + } + } + + access(all) fun createManager(tokens: @FlowToken.Vault, defaultRouterAddress: Address): @Manager { + return <- create Manager(tokens: <- tokens, defaultRouterAddress: defaultRouterAddress) + } + + init() { + let identifier = "ContractManager_".concat(self.account.address.toString()) + self.StoragePath = StoragePath(identifier: identifier)! + self.PublicPath = PublicPath(identifier: identifier)! + } +} \ No newline at end of file diff --git a/contracts/flowty-drops/DropFactory.cdc b/contracts/flowty-drops/DropFactory.cdc new file mode 100644 index 0000000..0bf25fa --- /dev/null +++ b/contracts/flowty-drops/DropFactory.cdc @@ -0,0 +1,75 @@ +import "FungibleToken" +import "MetadataViews" + +import "FlowtyDrops" +import "FlowtyActiveCheckers" +import "FlowtyAddressVerifiers" +import "FlowtyPricers" + +/* +The DropFactory is a contract that helps create common types of drops +*/ +access(all) contract DropFactory { + access(all) fun createEndlessOpenEditionDrop( + price: UFix64, + paymentTokenType: Type, + dropDisplay: MetadataViews.Display, + minterCap: Capability<&{FlowtyDrops.Minter}>, + nftTypeIdentifier: String + ): @FlowtyDrops.Drop { + pre { + paymentTokenType.isSubtype(of: Type<@{FungibleToken.Vault}>()): "paymentTokenType must be a FungibleToken" + } + + // This drop is always on and never ends. + let activeChecker = FlowtyActiveCheckers.AlwaysOn() + + // All addresses are allowed to participate + let addressVerifier = FlowtyAddressVerifiers.AllowAll(maxPerMint: 10) + + // The cost of each mint is the same, and only permits one token type as payment + let pricer = FlowtyPricers.FlatPrice(price: price, paymentTokenType: paymentTokenType) + + let phaseDetails = FlowtyDrops.PhaseDetails(activeChecker: activeChecker, display: nil, pricer: pricer, addressVerifier: addressVerifier) + let phase <- FlowtyDrops.createPhase(details: phaseDetails) + + + let nftType = CompositeType(nftTypeIdentifier) ?? panic("invalid nft type identifier") + let dropDetails = FlowtyDrops.DropDetails(display: dropDisplay, medias: nil, commissionRate: 0.05, nftType: nftTypeIdentifier) + let drop <- FlowtyDrops.createDrop(details: dropDetails, minterCap: minterCap, phases: <- [<-phase]) + + return <- drop + } + + access(all) fun createTimeBasedOpenEditionDrop( + price: UFix64, + paymentTokenType: Type, + dropDisplay: MetadataViews.Display, + minterCap: Capability<&{FlowtyDrops.Minter}>, + startUnix: UInt64?, + endUnix: UInt64?, + nftTypeIdentifier: String + ): @FlowtyDrops.Drop { + pre { + paymentTokenType.isSubtype(of: Type<@{FungibleToken.Vault}>()): "paymentTokenType must be a FungibleToken" + } + + // This ActiveChecker turns on at a set unix timestamp (or is on by default if nil), and ends at the specified end date if provided + let activeChecker = FlowtyActiveCheckers.TimestampChecker(start: startUnix, end: endUnix) + + // All addresses are allowed to participate + let addressVerifier = FlowtyAddressVerifiers.AllowAll(maxPerMint: 10) + + // The cost of each mint is the same, and only permits one token type as payment + let pricer = FlowtyPricers.FlatPrice(price: price, paymentTokenType: paymentTokenType) + + let phaseDetails = FlowtyDrops.PhaseDetails(activeChecker: activeChecker, display: nil, pricer: pricer, addressVerifier: addressVerifier) + let phase <- FlowtyDrops.createPhase(details: phaseDetails) + + let nftType = CompositeType(nftTypeIdentifier) ?? panic("invalid nft type identifier") + let dropDetails = FlowtyDrops.DropDetails(display: dropDisplay, medias: nil, commissionRate: 0.05, nftType: nftTypeIdentifier) + let drop <- FlowtyDrops.createDrop(details: dropDetails, minterCap: minterCap, phases: <- [<-phase]) + + return <- drop + } +} \ No newline at end of file diff --git a/contracts/flowty-drops/DropTypes.cdc b/contracts/flowty-drops/DropTypes.cdc new file mode 100644 index 0000000..ba37248 --- /dev/null +++ b/contracts/flowty-drops/DropTypes.cdc @@ -0,0 +1,282 @@ +import "FlowtyDrops" +import "MetadataViews" +import "ViewResolver" +import "AddressUtils" + +access(all) contract DropTypes { + access(all) struct Display { + access(all) let name: String + access(all) let description: String + access(all) let url: String + + init(_ display: MetadataViews.Display) { + self.name = display.name + self.description = display.description + self.url = display.thumbnail.uri() + } + } + + access(all) struct Media { + access(all) let url: String + access(all) let mediaType: String + + init(_ media: MetadataViews.Media) { + self.url = media.file.uri() + self.mediaType = media.mediaType + } + } + + access(all) struct DropSummary { + access(all) let id: UInt64 + access(all) let display: Display + access(all) let medias: [Media] + access(all) let totalMinted: Int + access(all) let minterCount: Int + access(all) let commissionRate: UFix64 + access(all) let nftType: String + + access(all) let address: Address? + access(all) let mintedByAddress: Int? + + access(all) let phases: [PhaseSummary] + + access(all) let blockTimestamp: UInt64 + access(all) let blockHeight: UInt64 + + init( + id: UInt64, + display: MetadataViews.Display, + medias: MetadataViews.Medias?, + totalMinted: Int, + minterCount: Int, + mintedByAddress: Int?, + commissionRate: UFix64, + nftType: Type, + address: Address?, + phases: [PhaseSummary] + ) { + self.id = id + self.display = Display(display) + + self.medias = [] + for m in medias?.items ?? [] { + self.medias.append(Media(m)) + } + + + self.totalMinted = totalMinted + self.commissionRate = commissionRate + self.minterCount = minterCount + self.mintedByAddress = mintedByAddress + self.nftType = nftType.identifier + self.address = address + self.phases = phases + + let b = getCurrentBlock() + self.blockHeight = b.height + self.blockTimestamp = UInt64(b.timestamp) + } + } + + access(all) struct Quote { + access(all) let price: UFix64 + access(all) let quantity: Int + access(all) let paymentIdentifier: String + access(all) let minter: Address? + + init(price: UFix64, quantity: Int, paymentIdentifier: String, minter: Address?) { + self.price = price + self.quantity = quantity + self.paymentIdentifier = paymentIdentifier + self.minter = minter + } + } + + access(all) struct PhaseSummary { + access(all) let id: UInt64 + access(all) let index: Int + + access(all) let activeCheckerType: String + access(all) let pricerType: String + access(all) let addressVerifierType: String + + access(all) let hasStarted: Bool + access(all) let hasEnded: Bool + access(all) let start: UInt64? + access(all) let end: UInt64? + + access(all) let paymentTypes: [String] + + access(all) let address: Address? + access(all) let remainingForAddress: Int? + + access(all) let quote: Quote? + + init( + index: Int, + phase: &{FlowtyDrops.PhasePublic}, + address: Address?, + totalMinted: Int?, + minter: Address?, + quantity: Int?, + paymentIdentifier: String? + ) { + self.index = index + self.id = phase.uuid + + let d: FlowtyDrops.PhaseDetails = phase.getDetails() + self.activeCheckerType = d.activeChecker.getType().identifier + self.pricerType = d.pricer.getType().identifier + self.addressVerifierType = d.addressVerifier.getType().identifier + + self.hasStarted = d.activeChecker.hasStarted() + self.hasEnded = d.activeChecker.hasEnded() + self.start = d.activeChecker.getStart() + self.end = d.activeChecker.getEnd() + + self.paymentTypes = [] + for pt in d.pricer.getPaymentTypes() { + self.paymentTypes.append(pt.identifier) + } + + if let addr = address { + self.address = address + self.remainingForAddress = d.addressVerifier.remainingForAddress(addr: addr, totalMinted: totalMinted ?? 0) + } else { + self.address = nil + self.remainingForAddress = nil + } + + if paymentIdentifier != nil && quantity != nil { + let price = d.pricer.getPrice(num: quantity!, paymentTokenType: CompositeType(paymentIdentifier!)!, minter: minter) + + self.quote = Quote(price: price, quantity: quantity!, paymentIdentifier: paymentIdentifier!, minter: minter) + } else { + self.quote = nil + } + } + } + + access(all) fun getDropSummary(nftTypeIdentifier: String, dropID: UInt64, minter: Address?, quantity: Int?, paymentIdentifier: String?): DropSummary? { + let nftType = CompositeType(nftTypeIdentifier) ?? panic("invalid nft type identifier") + let segments = nftTypeIdentifier.split(separator: ".") + let contractAddress = AddressUtils.parseAddress(nftType)! + let contractName = segments[2] + + let resolver = getAccount(contractAddress).contracts.borrow<&{ViewResolver}>(name: contractName) + if resolver == nil { + return nil + } + + let dropResolver = resolver!.resolveContractView(resourceType: nftType, viewType: Type()) as! FlowtyDrops.DropResolver? + if dropResolver == nil { + return nil + } + + let container = dropResolver!.borrowContainer() + if container == nil { + return nil + } + + let drop = container!.borrowDropPublic(id: dropID) + if drop == nil { + return nil + } + + let dropDetails = drop!.getDetails() + + let phaseSummaries: [PhaseSummary] = [] + for index, phase in drop!.borrowAllPhases() { + let summary = PhaseSummary( + index: index, + phase: phase, + address: minter, + totalMinted: minter != nil ? dropDetails.minters[minter!] : nil, + minter: minter, + quantity: quantity, + paymentIdentifier: paymentIdentifier + ) + phaseSummaries.append(summary) + } + + let dropSummary = DropSummary( + id: drop!.uuid, + display: dropDetails.display, + medias: dropDetails.medias, + totalMinted: dropDetails.totalMinted, + minterCount: dropDetails.minters.keys.length, + mintedByAddress: minter != nil ? dropDetails.minters[minter!] : nil, + commissionRate: dropDetails.commissionRate, + nftType: CompositeType(dropDetails.nftType)!, + address: minter, + phases: phaseSummaries + ) + + return dropSummary + } + + access(all) fun getAllDropSummaries(nftTypeIdentifier: String, minter: Address?, quantity: Int?, paymentIdentifier: String?): [DropSummary] { + let nftType = CompositeType(nftTypeIdentifier) ?? panic("invalid nft type identifier") + let segments = nftTypeIdentifier.split(separator: ".") + let contractAddress = AddressUtils.parseAddress(nftType)! + let contractName = segments[2] + + let resolver = getAccount(contractAddress).contracts.borrow<&{ViewResolver}>(name: contractName) + if resolver == nil { + return [] + } + + let dropResolver = resolver!.resolveContractView(resourceType: nftType, viewType: Type()) as! FlowtyDrops.DropResolver? + if dropResolver == nil { + return [] + } + + let container = dropResolver!.borrowContainer() + if container == nil { + return [] + } + + let summaries: [DropSummary] = [] + for id in container!.getIDs() { + let drop = container!.borrowDropPublic(id: id) + if drop == nil { + continue + } + + let dropDetails = drop!.getDetails() + + let phaseSummaries: [PhaseSummary] = [] + for index, phase in drop!.borrowAllPhases() { + let summary = PhaseSummary( + index: index, + phase: phase, + address: minter, + totalMinted: minter != nil ? dropDetails.minters[minter!] : nil, + minter: minter, + quantity: quantity, + paymentIdentifier: paymentIdentifier + ) + phaseSummaries.append(summary) + } + + if CompositeType(dropDetails.nftType) == nil { + continue + } + + summaries.append(DropSummary( + id: drop!.uuid, + display: dropDetails.display, + medias: dropDetails.medias, + totalMinted: dropDetails.totalMinted, + minterCount: dropDetails.minters.keys.length, + mintedByAddress: minter != nil ? dropDetails.minters[minter!] : nil, + commissionRate: dropDetails.commissionRate, + nftType: CompositeType(dropDetails.nftType)!, + address: minter, + phases: phaseSummaries + )) + } + + return summaries + } +} \ No newline at end of file diff --git a/contracts/flowty-drops/FlowtyActiveCheckers.cdc b/contracts/flowty-drops/FlowtyActiveCheckers.cdc new file mode 100644 index 0000000..14faba1 --- /dev/null +++ b/contracts/flowty-drops/FlowtyActiveCheckers.cdc @@ -0,0 +1,113 @@ +import "FlowtyDrops" + +/* +This contract contains implementations for the FlowtyDrops.ActiveChecker struct interface. +You can use these implementations, or your own, to configure when a phase in a drop is active +*/ +access(all) contract FlowtyActiveCheckers { + /* + The AlwaysOn ActiveChecker is always on and never ends. + */ + access(all) struct AlwaysOn: FlowtyDrops.ActiveChecker { + access(all) view fun hasStarted(): Bool { + return true + } + + access(all) view fun hasEnded(): Bool { + return false + } + + access(all) view fun getStart(): UInt64? { + return nil + } + + access(all) view fun getEnd(): UInt64? { + return nil + } + } + + /* + The manual checker is used to explicitly toggle a drop. + This version of checker allows a creator to turn on or off a drop at will + */ + access(all) struct ManualChecker: FlowtyDrops.ActiveChecker { + access(self) var started: Bool + access(self) var ended: Bool + + access(all) view fun hasStarted(): Bool { + return self.started + } + + access(all) view fun hasEnded(): Bool { + return self.ended + } + + access(all) view fun getStart(): UInt64? { + return nil + } + + access(all) view fun getEnd(): UInt64? { + return nil + } + + access(Mutate) fun setStarted(_ b: Bool) { + self.started = b + } + + access(Mutate) fun setEnded(_ b: Bool) { + self.ended = b + } + + init() { + self.started = false + self.ended = false + } + } + + /* + TimestampChecker uses block timestamps to determine if a phase or drop is live or not. + A timestamp checker has a start and an end time. + */ + access(all) struct TimestampChecker: FlowtyDrops.ActiveChecker { + access(all) var start: UInt64? + access(all) var end: UInt64? + + + access(all) view fun hasStarted(): Bool { + return self.start == nil || UInt64(getCurrentBlock().timestamp) >= self.start! + } + + access(all) view fun hasEnded(): Bool { + if self.end == nil { + return false + } + + return UInt64(getCurrentBlock().timestamp) > self.end! + } + + access(all) view fun getStart(): UInt64? { + return self.start + } + + access(all) view fun getEnd(): UInt64? { + return self.end + } + + access(Mutate) fun setStart(start: UInt64?) { + self.start = start + } + + access(Mutate) fun setEnd(end: UInt64?) { + self.end = end + } + + init(start: UInt64?, end: UInt64?) { + pre { + start == nil || end == nil || start! < end!: "start must be less than end" + } + + self.start = start + self.end = end + } + } +} \ No newline at end of file diff --git a/contracts/flowty-drops/FlowtyAddressVerifiers.cdc b/contracts/flowty-drops/FlowtyAddressVerifiers.cdc new file mode 100644 index 0000000..1fd6a3d --- /dev/null +++ b/contracts/flowty-drops/FlowtyAddressVerifiers.cdc @@ -0,0 +1,64 @@ +import "FlowtyDrops" + +/* +This contract contains implementations of the FlowtyDrops.AddressVerifier struct interface +*/ +access(all) contract FlowtyAddressVerifiers { + /* + The AllowAll AddressVerifier allows any address to mint without any verification + */ + access(all) struct AllowAll: FlowtyDrops.AddressVerifier { + access(all) var maxPerMint: Int + + access(all) view fun canMint(addr: Address, num: Int, totalMinted: Int, data: {String: AnyStruct}): Bool { + return num <= self.maxPerMint + } + + access(Mutate) fun setMaxPerMint(_ value: Int) { + self.maxPerMint = value + } + + init(maxPerMint: Int) { + pre { + maxPerMint > 0: "maxPerMint must be greater than 0" + } + + self.maxPerMint = maxPerMint + } + } + + /* + The AllowList Verifier only lets a configured set of addresses participate in a drop phase. The number + of mints per address is specified to allow more granular control of what each address is permitted to do. + */ + access(all) struct AllowList: FlowtyDrops.AddressVerifier { + access(self) let allowedAddresses: {Address: Int} + + access(all) view fun canMint(addr: Address, num: Int, totalMinted: Int, data: {String: AnyStruct}): Bool { + if let allowedMints = self.allowedAddresses[addr] { + return allowedMints >= num + totalMinted + } + + return false + } + + access(all) view fun remainingForAddress(addr: Address, totalMinted: Int): Int? { + if let allowedMints = self.allowedAddresses[addr] { + return allowedMints - totalMinted + } + return nil + } + + access(Mutate) fun setAddress(addr: Address, value: Int) { + self.allowedAddresses[addr] = value + } + + access(Mutate) fun removeAddress(addr: Address) { + self.allowedAddresses.remove(key: addr) + } + + init(allowedAddresses: {Address: Int}) { + self.allowedAddresses = allowedAddresses + } + } +} \ No newline at end of file diff --git a/contracts/flowty-drops/FlowtyDrops.cdc b/contracts/flowty-drops/FlowtyDrops.cdc new file mode 100644 index 0000000..e5da834 --- /dev/null +++ b/contracts/flowty-drops/FlowtyDrops.cdc @@ -0,0 +1,461 @@ +import "NonFungibleToken" +import "FungibleToken" +import "MetadataViews" +import "AddressUtils" +import "FungibleTokenMetadataViews" +import "FungibleTokenRouter" + +access(all) contract FlowtyDrops { + access(all) let ContainerStoragePath: StoragePath + access(all) let ContainerPublicPath: PublicPath + + access(all) event DropAdded(address: Address, id: UInt64, name: String, description: String, imageUrl: String, start: UInt64?, end: UInt64?, nftType: String) + access(all) event Minted(address: Address, dropID: UInt64, phaseID: UInt64, nftID: UInt64, nftType: String) + access(all) event PhaseAdded(dropID: UInt64, dropAddress: Address, id: UInt64, index: Int, activeCheckerType: String, pricerType: String, addressVerifierType: String) + access(all) event PhaseRemoved(dropID: UInt64, dropAddress: Address, id: UInt64) + + access(all) entitlement Owner + access(all) entitlement EditPhase + + // Interface to expose all the components necessary to participate in a drop + // and to ask questions about a drop. + access(all) resource interface DropPublic { + access(all) fun borrowPhasePublic(index: Int): &{PhasePublic} + access(all) fun borrowActivePhases(): [&{PhasePublic}] + access(all) fun borrowAllPhases(): [&{PhasePublic}] + access(all) fun mint( + payment: @{FungibleToken.Vault}, + amount: Int, + phaseIndex: Int, + expectedType: Type, + receiverCap: Capability<&{NonFungibleToken.CollectionPublic}>, + commissionReceiver: Capability<&{FungibleToken.Receiver}>?, + data: {String: AnyStruct} + ): @{FungibleToken.Vault} + access(all) fun getDetails(): DropDetails + } + + // A phase represents a stage of a drop. Some drops will only have one + // phase, while others could have many. For example, a drop with an allow list + // and a public mint would likely have two phases. + access(all) resource Phase: PhasePublic { + access(all) event ResourceDestroyed(uuid: UInt64 = self.uuid) + + access(all) let details: PhaseDetails + + access(all) let data: {String: AnyStruct} + access(all) let resources: @{String: AnyResource} + + // returns whether this phase of a drop has started. + access(all) fun isActive(): Bool { + return self.details.activeChecker.hasStarted() && !self.details.activeChecker.hasEnded() + } + + access(all) fun getDetails(): PhaseDetails { + return self.details + } + + access(EditPhase) fun borrowActiveCheckerAuth(): auth(Mutate) &{ActiveChecker} { + return &self.details.activeChecker + } + + access(EditPhase) fun borrowPricerAuth(): auth(Mutate) &{Pricer} { + return &self.details.pricer + } + + access(EditPhase) fun borrowAddressVerifierAuth(): auth(Mutate) &{AddressVerifier} { + return &self.details.addressVerifier + } + + init(details: PhaseDetails) { + self.details = details + + self.data = {} + self.resources <- {} + } + } + + access(all) resource Drop: DropPublic { + access(all) event ResourceDestroyed( + uuid: UInt64 = self.uuid, + minterAddress: Address = self.minterCap.address, + nftType: String = self.details.nftType, + totalMinted: Int = self.details.totalMinted + ) + + // phases represent the stages of a drop. For example, a drop might have an allowlist and a public mint phase. + access(self) let phases: @[Phase] + // the details of a drop. This includes things like display information and total number of mints + access(self) let details: DropDetails + access(self) let minterCap: Capability<&{Minter}> + + access(all) let data: {String: AnyStruct} + access(all) let resources: @{String: AnyResource} + + access(all) fun mint( + payment: @{FungibleToken.Vault}, + amount: Int, + phaseIndex: Int, + expectedType: Type, + receiverCap: Capability<&{NonFungibleToken.CollectionPublic}>, + commissionReceiver: Capability<&{FungibleToken.Receiver}>?, + data: {String: AnyStruct} + ): @{FungibleToken.Vault} { + pre { + expectedType.isSubtype(of: Type<@{NonFungibleToken.NFT}>()): "expected type must be an NFT" + expectedType.identifier == self.details.nftType: "expected type does not match drop details type" + self.phases.length > phaseIndex: "phase index is too high" + receiverCap.check(): "receiver capability is not valid" + } + + // validate the payment vault amount and type + let phase: &Phase = &self.phases[phaseIndex] + assert( + phase.details.addressVerifier.canMint(addr: receiverCap.address, num: amount, totalMinted: self.details.minters[receiverCap.address] ?? 0, data: {}), + message: "receiver address has exceeded their mint capacity" + ) + + let paymentAmount = phase.details.pricer.getPrice(num: amount, paymentTokenType: payment.getType(), minter: receiverCap.address) + let withdrawn <- payment.withdraw(amount: paymentAmount) // make sure that we have a fresh vault resource + + // take commission + if commissionReceiver != nil && commissionReceiver!.check() { + let commission <- withdrawn.withdraw(amount: self.details.commissionRate * withdrawn.balance) + commissionReceiver!.borrow()!.deposit(from: <-commission) + } + + assert(phase.details.pricer.getPrice(num: amount, paymentTokenType: withdrawn.getType(), minter: receiverCap.address) * (1.0 - self.details.commissionRate) == withdrawn.balance, message: "incorrect payment amount") + assert(phase.details.pricer.getPaymentTypes().contains(withdrawn.getType()), message: "unsupported payment type") + + // mint the nfts + let minter = self.minterCap.borrow() ?? panic("minter capability could not be borrowed") + let mintedNFTs: @[{NonFungibleToken.NFT}] <- minter.mint(payment: <-withdrawn, amount: amount, phase: phase, data: data) + assert(phase.details.activeChecker.hasStarted() && !phase.details.activeChecker.hasEnded(), message: "phase is not active") + assert(mintedNFTs.length == amount, message: "incorrect number of items returned") + + // distribute to receiver + let receiver = receiverCap.borrow() ?? panic("could not borrow receiver capability") + self.details.addMinted(num: mintedNFTs.length, addr: receiverCap.address) + + while mintedNFTs.length > 0 { + let nft <- mintedNFTs.removeFirst() + + let nftType = nft.getType() + emit Minted(address: receiverCap.address, dropID: self.uuid, phaseID: phase.uuid, nftID: nft.id, nftType: nftType.identifier) + + // validate that every nft is the right type + assert(nftType == expectedType, message: "unexpected nft type was minted") + + receiver.deposit(token: <-nft) + } + + // cleanup + destroy mintedNFTs + + // return excess payment + return <- payment + } + + access(Owner) fun borrowPhase(index: Int): auth(EditPhase) &Phase { + return &self.phases[index] + } + + + access(all) fun borrowPhasePublic(index: Int): &{PhasePublic} { + return &self.phases[index] + } + + access(all) fun borrowActivePhases(): [&{PhasePublic}] { + let arr: [&{PhasePublic}] = [] + var count = 0 + while count < self.phases.length { + let ref = self.borrowPhasePublic(index: count) + let activeChecker = ref.getDetails().activeChecker + if activeChecker.hasStarted() && !activeChecker.hasEnded() { + arr.append(ref) + } + + count = count + 1 + } + + return arr + } + + access(all) fun borrowAllPhases(): [&{PhasePublic}] { + let arr: [&{PhasePublic}] = [] + var index = 0 + while index < self.phases.length { + let ref = self.borrowPhasePublic(index: index) + arr.append(ref) + index = index + 1 + } + + return arr + } + + access(Owner) fun addPhase(_ phase: @Phase) { + emit PhaseAdded( + dropID: self.uuid, + dropAddress: self.owner!.address, + id: phase.uuid, + index: self.phases.length, + activeCheckerType: phase.details.activeChecker.getType().identifier, + pricerType: phase.details.pricer.getType().identifier, + addressVerifierType: phase.details.addressVerifier.getType().identifier + ) + self.phases.append(<-phase) + } + + access(Owner) fun removePhase(index: Int): @Phase { + pre { + self.phases.length > index: "index is greater than length of phases" + } + + let phase <- self.phases.remove(at: index) + emit PhaseRemoved(dropID: self.uuid, dropAddress: self.owner!.address, id: phase.uuid) + + return <- phase + } + + access(all) fun getDetails(): DropDetails { + return self.details + } + + init(details: DropDetails, minterCap: Capability<&{Minter}>, phases: @[Phase]) { + pre { + minterCap.check(): "minter capability is not valid" + } + + self.phases <- phases + self.details = details + self.minterCap = minterCap + + self.data = {} + self.resources <- {} + } + } + + access(all) struct DropDetails { + access(all) let display: MetadataViews.Display + access(all) let medias: MetadataViews.Medias? + access(all) var totalMinted: Int + access(all) var minters: {Address: Int} + access(all) let commissionRate: UFix64 + access(all) let nftType: String + + access(all) let data: {String: AnyStruct} + + access(contract) fun addMinted(num: Int, addr: Address) { + self.totalMinted = self.totalMinted + num + if self.minters[addr] == nil { + self.minters[addr] = 0 + } + + self.minters[addr] = self.minters[addr]! + num + } + + init(display: MetadataViews.Display, medias: MetadataViews.Medias?, commissionRate: UFix64, nftType: String) { + pre { + nftType != "": "nftType should be a composite type identifier" + } + + self.display = display + self.medias = medias + self.totalMinted = 0 + self.commissionRate = commissionRate + self.minters = {} + self.nftType = nftType + self.data = {} + } + } + + // An ActiveChecker represents a phase being on or off, and holds information + // about whether a phase has started or not. + access(all) struct interface ActiveChecker { + // Signal that a phase has started. If the phase has not ended, it means that this activeChecker's phase + // is active + access(all) view fun hasStarted(): Bool + // Signal that a phase has ended. If an ActiveChecker has ended, minting will not work. That could mean + // the drop is over, or it could mean another phase has begun. + access(all) view fun hasEnded(): Bool + + access(all) view fun getStart(): UInt64? + access(all) view fun getEnd(): UInt64? + } + + access(all) resource interface PhasePublic { + // What does a phase need to be able to answer/manage? + // - What are the details of the phase being interactive with? + // - How many items are left in the current phase? + // - Can Address x mint on a phase? + // - What is the cost to mint for the phase I am interested in (for address x)? + access(all) fun getDetails(): PhaseDetails + access(all) fun isActive(): Bool + } + + access(all) struct PhaseDetails { + // handles whether a phase is on or not + access(all) let activeChecker: {ActiveChecker} + + // display information about a phase + access(all) let display: MetadataViews.Display? + + // handles the pricing of a phase + access(all) let pricer: {Pricer} + + // verifies whether an address is able to mint + access(all) let addressVerifier: {AddressVerifier} + + // placecholder data dictionary to allow new fields to be accessed + access(all) let data: {String: AnyStruct} + + init(activeChecker: {ActiveChecker}, display: MetadataViews.Display?, pricer: {Pricer}, addressVerifier: {AddressVerifier}) { + self.activeChecker = activeChecker + self.display = display + self.pricer = pricer + self.addressVerifier = addressVerifier + + self.data = {} + } + } + + access(all) struct interface AddressVerifier { + access(all) fun canMint(addr: Address, num: Int, totalMinted: Int, data: {String: AnyStruct}): Bool { + return true + } + + access(all) fun remainingForAddress(addr: Address, totalMinted: Int): Int? { + return nil + } + } + + access(all) struct interface Pricer { + access(all) fun getPrice(num: Int, paymentTokenType: Type, minter: Address?): UFix64 + access(all) fun getPaymentTypes(): [Type] + } + + access(all) resource interface Minter { + access(contract) fun mint(payment: @{FungibleToken.Vault}, amount: Int, phase: &FlowtyDrops.Phase, data: {String: AnyStruct}): @[{NonFungibleToken.NFT}] { + let resourceAddress = AddressUtils.parseAddress(self.getType())! + let receiver = getAccount(resourceAddress).capabilities.get<&{FungibleToken.Receiver}>(FungibleTokenRouter.PublicPath).borrow() + ?? panic("missing receiver at fungible token router path") + receiver.deposit(from: <-payment) + + let nfts: @[{NonFungibleToken.NFT}] <- [] + + var count = 0 + while count < amount { + count = count + 1 + nfts.append(<- self.createNextNFT()) + } + + return <- nfts + } + + access(contract) fun createNextNFT(): @{NonFungibleToken.NFT} + } + + access(all) struct DropResolver { + access(self) let cap: Capability<&{ContainerPublic}> + + access(all) fun borrowContainer(): &{ContainerPublic}? { + return self.cap.borrow() + } + + init(cap: Capability<&{ContainerPublic}>) { + pre { + cap.check(): "container capability is not valid" + } + + self.cap = cap + } + } + + access(all) resource interface ContainerPublic { + access(all) fun borrowDropPublic(id: UInt64): &{DropPublic}? + access(all) fun getIDs(): [UInt64] + } + + // Contains drops. + access(all) resource Container: ContainerPublic { + access(self) let drops: @{UInt64: Drop} + + access(all) let data: {String: AnyStruct} + access(all) let resources: @{String: AnyResource} + + access(Owner) fun addDrop(_ drop: @Drop) { + let details = drop.getDetails() + + let phases = drop.borrowAllPhases() + assert(phases.length > 0, message: "drops must have at least one phase to be added to a container") + + let firstPhaseDetails = phases[0].getDetails() + + emit DropAdded( + address: self.owner!.address, + id: drop.uuid, + name: details.display.name, + description: details.display.description, + imageUrl: details.display.thumbnail.uri(), + start: firstPhaseDetails.activeChecker.getStart(), + end: firstPhaseDetails.activeChecker.getEnd(), + nftType: details.nftType + ) + destroy self.drops.insert(key: drop.uuid, <-drop) + } + + access(Owner) fun removeDrop(id: UInt64): @Drop { + pre { + self.drops.containsKey(id): "drop was not found" + } + + return <- self.drops.remove(key: id)! + } + + access(Owner) fun borrowDrop(id: UInt64): auth(Owner) &Drop? { + return &self.drops[id] + } + + access(all) fun borrowDropPublic(id: UInt64): &{DropPublic}? { + return &self.drops[id] + } + + access(all) fun getIDs(): [UInt64] { + return self.drops.keys + } + + init() { + self.drops <- {} + + self.data = {} + self.resources <- {} + } + } + + access(all) fun createPhase(details: PhaseDetails): @Phase { + return <- create Phase(details: details) + } + + access(all) fun createDrop(details: DropDetails, minterCap: Capability<&{Minter}>, phases: @[Phase]): @Drop { + return <- create Drop(details: details, minterCap: minterCap, phases: <- phases) + } + + access(all) fun createContainer(): @Container { + return <- create Container() + } + + access(all) fun getMinterStoragePath(type: Type): StoragePath { + let segments = type.identifier.split(separator: ".") + let identifier = "FlowtyDrops_Minter_".concat(segments[1]).concat("_").concat(segments[2]) + return StoragePath(identifier: identifier)! + } + + init() { + let identifier = "FlowtyDrops_".concat(self.account.address.toString()) + let containerIdentifier = identifier.concat("_Container") + let minterIdentifier = identifier.concat("_Minter") + + self.ContainerStoragePath = StoragePath(identifier: containerIdentifier)! + self.ContainerPublicPath = PublicPath(identifier: containerIdentifier)! + } +} \ No newline at end of file diff --git a/contracts/flowty-drops/FlowtyPricers.cdc b/contracts/flowty-drops/FlowtyPricers.cdc new file mode 100644 index 0000000..25555f0 --- /dev/null +++ b/contracts/flowty-drops/FlowtyPricers.cdc @@ -0,0 +1,48 @@ +import "FlowtyDrops" +import "FlowToken" + +/* +This contract contains implementations of the FlowtyDrops.Pricer interface. +You can use these, or any custom implementation for the phases of your drop. +*/ +access(all) contract FlowtyPricers { + + /* + The FlatPrice Pricer implementation has a set price and token type. Every mint is the same cost regardless of + the number minter, or what address is minting + */ + access(all) struct FlatPrice: FlowtyDrops.Pricer { + access(all) var price: UFix64 + access(all) let paymentTokenType: String + + access(all) view fun getPrice(num: Int, paymentTokenType: Type, minter: Address?): UFix64 { + return self.price * UFix64(num) + } + + access(all) view fun getPaymentTypes(): [Type] { + return [CompositeType(self.paymentTokenType)!] + } + + access(Mutate) fun setPrice(price: UFix64) { + self.price = price + } + + init(price: UFix64, paymentTokenType: Type) { + self.price = price + self.paymentTokenType = paymentTokenType.identifier + } + } + + /* + The Free Pricer can be used for a free mint, it has no price and always marks its payment type as @FlowToken.Vault + */ + access(all) struct Free: FlowtyDrops.Pricer { + access(all) fun getPrice(num: Int, paymentTokenType: Type, minter: Address?): UFix64 { + return 0.0 + } + + access(all) fun getPaymentTypes(): [Type] { + return [Type<@FlowToken.Vault>()] + } + } +} \ No newline at end of file diff --git a/contracts/flowty-drops/initializers/ContractBorrower.cdc b/contracts/flowty-drops/initializers/ContractBorrower.cdc new file mode 100644 index 0000000..5506c87 --- /dev/null +++ b/contracts/flowty-drops/initializers/ContractBorrower.cdc @@ -0,0 +1,14 @@ +import "FlowtyDrops" +import "NFTMetadata" +import "AddressUtils" +import "ContractInitializer" + +access(all) contract ContractBorrower { + access(all) fun borrowInitializer(typeIdentifier: String): &{ContractInitializer} { + let type = CompositeType(typeIdentifier) ?? panic("invalid type identifier") + let addr = AddressUtils.parseAddress(type)! + + let contractName = type.identifier.split(separator: ".")[2] + return getAccount(addr).contracts.borrow<&{ContractInitializer}>(name: contractName)! + } +} \ No newline at end of file diff --git a/contracts/flowty-drops/initializers/ContractInitializer.cdc b/contracts/flowty-drops/initializers/ContractInitializer.cdc new file mode 100644 index 0000000..c7acd3b --- /dev/null +++ b/contracts/flowty-drops/initializers/ContractInitializer.cdc @@ -0,0 +1,7 @@ +import "FlowtyDrops" +import "NFTMetadata" +import "AddressUtils" + +access(all) contract interface ContractInitializer { + access(all) fun initialize(contractAcct: auth(Storage, Contracts, Keys, Inbox, Capabilities) &Account, params: {String: AnyStruct}): NFTMetadata.InitializedCaps +} \ No newline at end of file diff --git a/contracts/flowty-drops/initializers/OpenEditionInitializer.cdc b/contracts/flowty-drops/initializers/OpenEditionInitializer.cdc new file mode 100644 index 0000000..fd001d2 --- /dev/null +++ b/contracts/flowty-drops/initializers/OpenEditionInitializer.cdc @@ -0,0 +1,57 @@ +import "ContractInitializer" +import "NFTMetadata" +import "FlowtyDrops" +import "NonFungibleToken" +import "UniversalCollection" + +access(all) contract OpenEditionInitializer: ContractInitializer { + access(all) fun initialize(contractAcct: auth(Storage, Contracts, Keys, Inbox, Capabilities) &Account, params: {String: AnyStruct}): NFTMetadata.InitializedCaps { + pre { + params["data"] != nil: "missing param data" + params["data"]!.getType() == Type(): "data param must be of type NFTMetadata.Metadata" + params["collectionInfo"] != nil: "missing param collectionInfo" + params["collectionInfo"]!.getType() == Type(): "collectionInfo param must be of type NFTMetadata.CollectionInfo" + params["type"] != nil: "missing param type" + params["type"]!.getType() == Type(): "type param must be of type Type" + } + + let data = params["data"]! as! NFTMetadata.Metadata + let collectionInfo = params["collectionInfo"]! as! NFTMetadata.CollectionInfo + + let nftType = params["type"]! as! Type + let contractName = nftType.identifier.split(separator: ".")[2] + + // do we have information to setup a drop as well? + if params.containsKey("dropDetails") && params.containsKey("phaseDetails") && params.containsKey("minterController") { + // extract expected keys + let minterCap = params["minterController"]! as! Capability<&{FlowtyDrops.Minter}> + let dropDetails = params["dropDetails"]! as! FlowtyDrops.DropDetails + let phaseDetails = params["phaseDetails"]! as! [FlowtyDrops.PhaseDetails] + + assert(minterCap.check(), message: "invalid minter capability") + assert(CompositeType(dropDetails.nftType) != nil, message: "dropDetails.nftType must be a valid CompositeType") + + let phases: @[FlowtyDrops.Phase] <- [] + for p in phaseDetails { + phases.append(<- FlowtyDrops.createPhase(details: p)) + } + + let drop <- FlowtyDrops.createDrop(details: dropDetails, minterCap: minterCap, phases: <- phases) + if contractAcct.storage.borrow<&AnyResource>(from: FlowtyDrops.ContainerStoragePath) == nil { + contractAcct.storage.save(<- FlowtyDrops.createContainer(), to: FlowtyDrops.ContainerStoragePath) + + contractAcct.capabilities.unpublish(FlowtyDrops.ContainerPublicPath) + contractAcct.capabilities.publish( + contractAcct.capabilities.storage.issue<&{FlowtyDrops.ContainerPublic}>(FlowtyDrops.ContainerStoragePath), + at: FlowtyDrops.ContainerPublicPath + ) + } + + let container = contractAcct.storage.borrow(from: FlowtyDrops.ContainerStoragePath) + ?? panic("drops container not found") + container.addDrop(<- drop) + } + + return NFTMetadata.initialize(acct: contractAcct, collectionInfo: collectionInfo, nftType: nftType) + } +} \ No newline at end of file diff --git a/contracts/flowty-drops/nft/BaseCollection.cdc b/contracts/flowty-drops/nft/BaseCollection.cdc new file mode 100644 index 0000000..7a9203f --- /dev/null +++ b/contracts/flowty-drops/nft/BaseCollection.cdc @@ -0,0 +1,97 @@ +import "NonFungibleToken" +import "ViewResolver" +import "MetadataViews" +import "NFTMetadata" +import "FlowtyDrops" +import "AddressUtils" +import "StringUtils" + +access(all) contract interface BaseCollection: ViewResolver { + access(all) var MetadataCap: Capability<&NFTMetadata.Container> + access(all) var totalSupply: UInt64 + + access(all) fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection} + + // The base collection is an interface that attmepts to take more boilerplate + // off of NFT-standard compliant definitions. + access(all) resource interface Collection: NonFungibleToken.Collection { + access(all) var ownedNFTs: @{UInt64: {NonFungibleToken.NFT}} + access(all) var nftType: Type + + access(all) fun deposit(token: @{NonFungibleToken.NFT}) { + pre { + token.getType() == self.nftType: "unexpected nft type being deposited" + } + + destroy self.ownedNFTs.insert(key: token.uuid, <-token) + } + + access(all) view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}? { + return &self.ownedNFTs[id] + } + + access(all) view fun getSupportedNFTTypes(): {Type: Bool} { + return { + self.nftType: true + } + } + + access(all) view fun isSupportedNFTType(type: Type): Bool { + return type == self.nftType + } + + access(NonFungibleToken.Withdraw) fun withdraw(withdrawID: UInt64): @{NonFungibleToken.NFT} { + return <- self.ownedNFTs.remove(key: withdrawID)! + } + } + + access(all) view fun getContractViews(resourceType: Type?): [Type] { + return [ + Type(), + Type() + ] + } + + access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? { + if resourceType == nil { + return nil + } + + let rt = resourceType! + let segments = rt.identifier.split(separator: ".") + let pathIdentifier = StringUtils.join([segments[2], segments[1]], "_") + + let addr = AddressUtils.parseAddress(rt)! + let acct = getAccount(addr) + + switch viewType { + case Type(): + let segments = rt.identifier.split(separator: ".") + let pathIdentifier = StringUtils.join([segments[2], segments[1]], "_") + + return MetadataViews.NFTCollectionData( + storagePath: StoragePath(identifier: pathIdentifier)!, + publicPath: PublicPath(identifier: pathIdentifier)!, + publicCollection: Type<&{NonFungibleToken.Collection}>(), + publicLinkedType: Type<&{NonFungibleToken.Collection}>(), + createEmptyCollectionFunction: fun(): @{NonFungibleToken.Collection} { + let addr = AddressUtils.parseAddress(rt)! + let c = getAccount(addr).contracts.borrow<&{BaseCollection}>(name: segments[2])! + return <- c.createEmptyCollection(nftType: rt) + } + ) + case Type(): + let c = getAccount(addr).contracts.borrow<&{BaseCollection}>(name: segments[2])! + let tmp = c.MetadataCap.borrow() + if tmp == nil { + return nil + } + + return tmp!.collectionInfo.getDisplay() + case Type(): + return FlowtyDrops.DropResolver(cap: acct.capabilities.get<&{FlowtyDrops.ContainerPublic}>(FlowtyDrops.ContainerPublicPath)) + } + + return nil + } +} \ No newline at end of file diff --git a/contracts/flowty-drops/nft/BaseNFT.cdc b/contracts/flowty-drops/nft/BaseNFT.cdc new file mode 100644 index 0000000..bda3e66 --- /dev/null +++ b/contracts/flowty-drops/nft/BaseNFT.cdc @@ -0,0 +1,107 @@ +import "NonFungibleToken" +import "StringUtils" +import "AddressUtils" +import "ViewResolver" +import "MetadataViews" +import "BaseCollection" +import "FlowtyDrops" +import "NFTMetadata" +import "UniversalCollection" + +// A few primary challenges that have come up in thinking about how to define base-level interfaces +// for collections and NFTs: +// +// - How do we resolve contract-level interfaces? +// - How do we track total supply/serial numbers for NFTs? +// - How do we store traits and medias? +// +// For some of these, mainly contract-level interfaces, we might be able to simply consolidate +// all of these into one contract interface and require that collection display (banner, thumbnail, name, description, etc.) +// be stored at the top-level of the contract so that they can be easily referenced later. This could make things easier in that we can +// make a base definition for anyone to use, but since it isn't a concrete definition, anyone can later override the pre-generated +// pieces to and modify the code to their liking. This could achieve the best of both worlds where there is minimal work to get something +// off the ground, but doesn't close the door to customization in the future. This could come at the cost of duplicated resource definitions, +// or could have the risk of circular imports depending on how we resolve certain pieces of information about a collection. +access(all) contract interface BaseNFT: ViewResolver { + access(all) resource interface NFT: NonFungibleToken.NFT { + // This is the id entry that corresponds to an NFTs NFTMetadata.Container entry. + // Some NFTs might share the same data, so we want to permit reusing storage where possible + access(all) metadataID: UInt64 + + access(all) view fun getViews(): [Type] { + return [ + Type(), + Type(), + Type(), + Type(), + Type(), + Type(), + Type() + ] + } + + access(all) fun resolveView(_ view: Type): AnyStruct? { + if view == Type() { + return self.id + } + + let rt = self.getType() + let segments = rt.identifier.split(separator: ".") + let addr = AddressUtils.parseAddress(rt)! + let tmp = getAccount(addr).contracts.borrow<&{BaseCollection}>(name: segments[2]) + if tmp == nil { + return nil + } + + let c = tmp! + let tmpMd = c.MetadataCap.borrow() + if tmpMd == nil { + return nil + } + + let md = tmpMd! + switch view { + case Type(): + let pathIdentifier = StringUtils.join([segments[2], segments[1]], "_") + return MetadataViews.NFTCollectionData( + storagePath: StoragePath(identifier: pathIdentifier)!, + publicPath: PublicPath(identifier: pathIdentifier)!, + publicCollection: Type<&{NonFungibleToken.Collection}>(), + publicLinkedType: Type<&{NonFungibleToken.Collection}>(), + createEmptyCollectionFunction: fun(): @{NonFungibleToken.Collection} { + let addr = AddressUtils.parseAddress(rt)! + let c = getAccount(addr).contracts.borrow<&{BaseCollection}>(name: segments[2])! + return <- c.createEmptyCollection(nftType: rt) + } + ) + case Type(): + return md.collectionInfo.collectionDisplay + } + + if let entry = md.borrowMetadata(id: self.metadataID) { + switch view { + case Type(): + return entry.traits + case Type(): + return entry.editions + case Type(): + let num = (entry.editions?.infoList?.length ?? 0) > 0 ? entry.editions!.infoList[0].number : self.id + + return MetadataViews.Display( + name: entry.name.concat(" #").concat(num.toString()), + description: entry.description, + thumbnail: NFTMetadata.UriFile(entry.thumbnail.uri()) + ) + case Type(): + return entry.externalURL + } + } + + return nil + } + + access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} { + return <- UniversalCollection.createCollection(nftType: self.getType()) + } + } +} \ No newline at end of file diff --git a/contracts/flowty-drops/nft/ContractFactory.cdc b/contracts/flowty-drops/nft/ContractFactory.cdc new file mode 100644 index 0000000..8bc696e --- /dev/null +++ b/contracts/flowty-drops/nft/ContractFactory.cdc @@ -0,0 +1,13 @@ +import "ContractFactoryTemplate" +import "AddressUtils" + +access(all) contract ContractFactory { + access(all) fun createContract(templateType: Type, acct: auth(Contracts) &Account, name: String, params: {String: AnyStruct}, initializeIdentifier: String) { + let templateAddr = AddressUtils.parseAddress(templateType)! + let contractName = templateType.identifier.split(separator: ".")[2] + let templateContract = getAccount(templateAddr).contracts.borrow<&{ContractFactoryTemplate}>(name: contractName) + ?? panic("provided type is not a ContractTemplateFactory") + + templateContract.createContract(acct: acct, name: name, params: params, initializeIdentifier: initializeIdentifier) + } +} \ No newline at end of file diff --git a/contracts/flowty-drops/nft/ContractFactoryTemplate.cdc b/contracts/flowty-drops/nft/ContractFactoryTemplate.cdc new file mode 100644 index 0000000..23ac5a1 --- /dev/null +++ b/contracts/flowty-drops/nft/ContractFactoryTemplate.cdc @@ -0,0 +1,48 @@ +import "NonFungibleToken" +import "MetadataViews" +import "ViewResolver" + +import "FlowtyDrops" +import "BaseNFT" +import "BaseCollection" +import "NFTMetadata" +import "UniversalCollection" +import "ContractBorrower" + +import "AddressUtils" + +access(all) contract interface ContractFactoryTemplate { + access(all) fun createContract(acct: auth(Contracts) &Account, name: String, params: {String: AnyStruct}, initializeIdentifier: String) + + access(all) fun getContractAddresses(): {String: Address} { + let d: {String: Address} = { + "NonFungibleToken": AddressUtils.parseAddress(Type<&{NonFungibleToken}>())!, + "MetadataViews": AddressUtils.parseAddress(Type<&MetadataViews>())!, + "ViewResolver": AddressUtils.parseAddress(Type<&{ViewResolver}>())!, + "FlowtyDrops": AddressUtils.parseAddress(Type<&FlowtyDrops>())!, + "BaseNFT": AddressUtils.parseAddress(Type<&{BaseNFT}>())!, + "BaseCollection": AddressUtils.parseAddress(Type<&{BaseCollection}>())!, + "NFTMetadata": AddressUtils.parseAddress(Type<&NFTMetadata>())!, + "UniversalCollection": AddressUtils.parseAddress(Type<&UniversalCollection>())!, + "BaseCollection": AddressUtils.parseAddress(Type<&{BaseCollection}>())!, + "AddressUtils": AddressUtils.parseAddress(Type<&AddressUtils>())!, + "ContractBorrower": AddressUtils.parseAddress(Type())! + } + + return d + } + + access(all) fun importLine(name: String, addr: Address): String { + return "import ".concat(name).concat(" from ").concat(addr.toString()).concat("\n") + } + + access(all) fun generateImports(names: [String]): String { + let addresses = self.getContractAddresses() + var imports = "" + for n in names { + imports = imports.concat(self.importLine(name: n, addr: addresses[n] ?? panic("missing contract import address: ".concat(n)))) + } + + return imports + } +} \ No newline at end of file diff --git a/contracts/flowty-drops/nft/NFTMetadata.cdc b/contracts/flowty-drops/nft/NFTMetadata.cdc new file mode 100644 index 0000000..bef4635 --- /dev/null +++ b/contracts/flowty-drops/nft/NFTMetadata.cdc @@ -0,0 +1,140 @@ +import "NonFungibleToken" +import "MetadataViews" + +access(all) contract NFTMetadata { + access(all) entitlement Owner + + access(all) event MetadataFrozen(uuid: UInt64, owner: Address?) + + access(all) struct CollectionInfo { + access(all) var collectionDisplay: MetadataViews.NFTCollectionDisplay + + access(all) let data: {String: AnyStruct} + + access(all) fun getDisplay(): MetadataViews.NFTCollectionDisplay { + return self.collectionDisplay + } + + init(collectionDisplay: MetadataViews.NFTCollectionDisplay) { + self.collectionDisplay = collectionDisplay + + self.data = {} + } + } + + access(all) struct Metadata { + // these are used to create the display metadata view so that we can concatenate + // the id onto it. + access(all) let name: String + access(all) let description: String + access(all) let thumbnail: {MetadataViews.File} + + access(all) let traits: MetadataViews.Traits? + access(all) let editions: MetadataViews.Editions? + access(all) let externalURL: MetadataViews.ExternalURL? + access(all) let royalties: MetadataViews.Royalties? + + access(all) let data: {String: AnyStruct} + + init( + name: String, + description: String, + thumbnail: {MetadataViews.File}, + traits: MetadataViews.Traits?, + editions: MetadataViews.Editions?, + externalURL: MetadataViews.ExternalURL?, + royalties: MetadataViews.Royalties?, + data: {String: AnyStruct} + ) { + self.name = name + self.description = description + self.thumbnail = thumbnail + + self.traits = traits + self.editions = editions + self.externalURL = externalURL + self.royalties = royalties + + self.data = {} + } + } + + access(all) resource Container { + access(all) var collectionInfo: CollectionInfo + access(all) let metadata: {UInt64: Metadata} + access(all) var frozen: Bool + + access(all) let data: {String: AnyStruct} + access(all) let resources: @{String: AnyResource} + + access(all) fun borrowMetadata(id: UInt64): &Metadata? { + return &self.metadata[id] + } + + access(Owner) fun addMetadata(id: UInt64, data: Metadata) { + pre { + self.metadata[id] == nil: "id already has metadata assigned" + } + + self.metadata[id] = data + } + + access(Owner) fun freeze() { + self.frozen = true + emit MetadataFrozen(uuid: self.uuid, owner: self.owner?.address) + } + + init(collectionInfo: CollectionInfo) { + self.collectionInfo = collectionInfo + self.metadata = {} + self.frozen = false + + self.data = {} + self.resources <- {} + } + } + + access(all) struct InitializedCaps { + access(all) let pubCap: Capability<&Container> + access(all) let ownerCap: Capability + + access(all) let data: {String: AnyStruct} + + init(pubCap: Capability<&Container>, ownerCap: Capability) { + self.pubCap = pubCap + self.ownerCap = ownerCap + + self.data = {} + } + } + + access(all) fun createContainer(collectionInfo: CollectionInfo): @Container { + return <- create Container(collectionInfo: collectionInfo) + } + + access(all) fun initialize(acct: auth(Storage, Capabilities) &Account, collectionInfo: CollectionInfo, nftType: Type): InitializedCaps { + let storagePath = self.getCollectionStoragePath(type: nftType) + let container <- self.createContainer(collectionInfo: collectionInfo) + acct.storage.save(<-container, to: storagePath) + let pubCap = acct.capabilities.storage.issue<&Container>(storagePath) + let ownerCap = acct.capabilities.storage.issue(storagePath) + return InitializedCaps(pubCap: pubCap, ownerCap: ownerCap) + } + + access(all) struct UriFile: MetadataViews.File { + access(self) let url: String + + access(all) view fun uri(): String { + return self.url + } + + init(_ url: String) { + self.url = url + } + } + + access(all) fun getCollectionStoragePath(type: Type): StoragePath { + let segments = type.identifier.split(separator: ".") + return StoragePath(identifier: "NFTMetadataContainer_".concat(segments[2]).concat("_").concat(segments[1]))! + } +} \ No newline at end of file diff --git a/contracts/flowty-drops/nft/OpenEditionNFT.cdc b/contracts/flowty-drops/nft/OpenEditionNFT.cdc new file mode 100644 index 0000000..450c759 --- /dev/null +++ b/contracts/flowty-drops/nft/OpenEditionNFT.cdc @@ -0,0 +1,42 @@ +import "NonFungibleToken" +import "FlowtyDrops" +import "BaseNFT" +import "NFTMetadata" +import "UniversalCollection" +import "ContractBorrower" +import "BaseCollection" + +access(all) contract OpenEditionNFT: BaseCollection { + access(all) var MetadataCap: Capability<&NFTMetadata.Container> + access(all) var totalSupply: UInt64 + + access(all) resource NFT: BaseNFT.NFT { + access(all) let id: UInt64 + access(all) let metadataID: UInt64 + + init() { + OpenEditionNFT.totalSupply = OpenEditionNFT.totalSupply + 1 + self.id = OpenEditionNFT.totalSupply + self.metadataID = 0 + } + } + + access(all) resource NFTMinter: FlowtyDrops.Minter { + access(contract) fun createNextNFT(): @{NonFungibleToken.NFT} { + return <- create NFT() + } + } + + access(all) fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection} { + return <- UniversalCollection.createCollection(nftType: Type<@NFT>()) + } + + init(params: {String: AnyStruct}, initializeIdentifier: String) { + self.totalSupply = 0 + self.account.storage.save(<- create NFTMinter(), to: FlowtyDrops.getMinterStoragePath(type: self.getType())) + params["minterController"] = self.account.capabilities.storage.issue<&{FlowtyDrops.Minter}>(FlowtyDrops.getMinterStoragePath(type: self.getType())) + params["type"] = Type<@NFT>() + + self.MetadataCap = ContractBorrower.borrowInitializer(typeIdentifier: initializeIdentifier).initialize(contractAcct: self.account, params: params).pubCap + } +} \ No newline at end of file diff --git a/contracts/flowty-drops/nft/OpenEditionTemplate.cdc b/contracts/flowty-drops/nft/OpenEditionTemplate.cdc new file mode 100644 index 0000000..ef814f3 --- /dev/null +++ b/contracts/flowty-drops/nft/OpenEditionTemplate.cdc @@ -0,0 +1,54 @@ +import "ContractFactoryTemplate" +import "MetadataViews" +import "NFTMetadata" + +access(all) contract OpenEditionTemplate: ContractFactoryTemplate { + access(all) fun createContract(acct: auth(Contracts) &Account, name: String, params: {String: AnyStruct}, initializeIdentifier: String) { + let code = self.generateImports(names: [ + "NonFungibleToken", + "FlowtyDrops", + "BaseNFT", + "NFTMetadata", + "UniversalCollection", + "ContractBorrower", + "BaseCollection", + "ViewResolver" + ]).concat("\n\n") + .concat("access(all) contract ").concat(name).concat(": BaseCollection, ViewResolver {\n") + .concat(" access(all) var MetadataCap: Capability<&NFTMetadata.Container>\n") + .concat(" access(all) var totalSupply: UInt64\n") + .concat("\n\n") + .concat(" access(all) resource NFT: BaseNFT.NFT {\n") + .concat(" access(all) let id: UInt64\n") + .concat(" access(all) let metadataID: UInt64\n") + .concat("\n\n") + .concat(" init() {\n") + .concat(" ").concat(name).concat(".totalSupply = ").concat(name).concat(".totalSupply + 1\n") + .concat(" self.id = ").concat(name).concat(".totalSupply\n") + .concat(" self.metadataID = 0\n") + .concat(" }\n") + .concat(" }\n") + .concat(" access(all) resource NFTMinter: FlowtyDrops.Minter {\n") + .concat(" access(contract) fun createNextNFT(): @{NonFungibleToken.NFT} {\n") + .concat(" return <- create NFT()\n") + .concat(" }\n") + .concat(" }\n") + .concat("\n") + .concat(" access(all) fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection} {\n") + .concat(" return <- UniversalCollection.createCollection(nftType: Type<@NFT>())\n") + .concat(" }\n") + .concat("\n") + .concat(" init(params: {String: AnyStruct}, initializeIdentifier: String) {\n") + .concat(" self.totalSupply = 0\n") + .concat(" let minter <- create NFTMinter()\n") + .concat(" self.account.storage.save(<-minter, to: FlowtyDrops.getMinterStoragePath(type: self.getType()))\n") + .concat(" params[\"minterController\"] = self.account.capabilities.storage.issue<&{FlowtyDrops.Minter}>(FlowtyDrops.getMinterStoragePath(type: self.getType()))\n") + .concat(" params[\"type\"] = Type<@NFT>()\n") + .concat("\n\n") + .concat(" self.MetadataCap = ContractBorrower.borrowInitializer(typeIdentifier: initializeIdentifier).initialize(contractAcct: self.account, params: params).pubCap\n") + .concat(" }\n") + .concat("}\n") + + acct.contracts.add(name: name, code: code.utf8, params, initializeIdentifier) + } +} diff --git a/contracts/flowty-drops/nft/UniversalCollection.cdc b/contracts/flowty-drops/nft/UniversalCollection.cdc new file mode 100644 index 0000000..9f53021 --- /dev/null +++ b/contracts/flowty-drops/nft/UniversalCollection.cdc @@ -0,0 +1,29 @@ +import "NonFungibleToken" +import "MetadataViews" +import "BaseCollection" + +access(all) contract UniversalCollection { + access(all) resource Collection: BaseCollection.Collection { + access(all) var ownedNFTs: @{UInt64: {NonFungibleToken.NFT}} + access(all) var nftType: Type + + access(all) let data: {String: AnyStruct} + access(all) let resources: @{String: AnyResource} + + access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} { + return <- create Collection(nftType: self.nftType) + } + + init (nftType: Type) { + self.ownedNFTs <- {} + self.nftType = nftType + + self.data = {} + self.resources <- {} + } + } + + access(all) fun createCollection(nftType: Type): @Collection { + return <- create Collection(nftType: nftType) + } +} \ No newline at end of file diff --git a/contracts/fungible-token-router/FungibleTokenRouter.cdc b/contracts/fungible-token-router/FungibleTokenRouter.cdc new file mode 100644 index 0000000..63fc0b1 --- /dev/null +++ b/contracts/fungible-token-router/FungibleTokenRouter.cdc @@ -0,0 +1,103 @@ +/* +FungibleTokenRouter forwards tokens from one account to another using +FungibleToken metadata views. If a token is not configured to be received, +any deposits will panic like they would a deposit that it attempt to a +non-existent receiver + +https://github.com/Flowtyio/fungible-token-router +*/ + +import "FungibleToken" +import "FungibleTokenMetadataViews" +import "FlowToken" + +access(all) contract FungibleTokenRouter { + access(all) let StoragePath: StoragePath + access(all) let PublicPath: PublicPath + + access(all) entitlement Owner + + access(all) event RouterCreated(uuid: UInt64, defaultAddress: Address) + access(all) event OverrideAdded(uuid: UInt64, owner: Address?, overrideAddress: Address, tokenType: String) + access(all) event OverrideRemoved(uuid: UInt64, owner: Address?, overrideAddress: Address?, tokenType: String) + access(all) event TokensRouted(tokenType: String, amount: UFix64, to: Address) + + access(all) resource Router: FungibleToken.Receiver { + // a default address that is used for any token type that is not overridden + access(all) var defaultAddress: Address + + // token type identifier -> destination address + access(all) var addressOverrides: {String: Address} + + access(Owner) fun setDefaultAddress(_ addr: Address) { + self.defaultAddress = addr + } + + access(Owner) fun addOverride(type: Type, addr: Address) { + emit OverrideAdded(uuid: self.uuid, owner: self.owner?.address, overrideAddress: addr, tokenType: type.identifier) + self.addressOverrides[type.identifier] = addr + } + + access(Owner) fun removeOverride(type: Type): Address? { + let removedAddr = self.addressOverrides.remove(key: type.identifier) + emit OverrideRemoved(uuid: self.uuid, owner: self.owner?.address, overrideAddress: removedAddr, tokenType: type.identifier) + return removedAddr + } + + access(all) fun deposit(from: @{FungibleToken.Vault}) { + let tokenType = from.getType() + let destination = self.addressOverrides[tokenType.identifier] ?? self.defaultAddress + + var vaultDataOpt: FungibleTokenMetadataViews.FTVaultData? = nil + + if tokenType == Type<@FlowToken.Vault>() { + vaultDataOpt = FungibleTokenMetadataViews.FTVaultData( + storagePath: /storage/flowTokenVault, + receiverPath: /public/flowTokenReceiver, + metadataPath: /public/flowTokenReceiver, + receiverLinkedType: Type<&FlowToken.Vault>(), + metadataLinkedType: Type<&FlowToken.Vault>(), + createEmptyVaultFunction: fun(): @{FungibleToken.Vault} { + return <- FlowToken.createEmptyVault(vaultType: tokenType) + } + ) + } else if let md = from.resolveView(Type()) { + vaultDataOpt = md as! FungibleTokenMetadataViews.FTVaultData + } + + let vaultData = vaultDataOpt ?? panic("vault data could not be retrieved for type ".concat(tokenType.identifier)) + let receiver = getAccount(destination).capabilities.get<&{FungibleToken.Receiver}>(vaultData.receiverPath) + assert(receiver.check(), message: "no receiver found at path: ".concat(vaultData.receiverPath.toString())) + + emit TokensRouted(tokenType: tokenType.identifier, amount: from.balance, to: destination) + receiver.borrow()!.deposit(from: <-from) + } + + access(all) view fun getSupportedVaultTypes(): {Type: Bool} { + // theoretically any token is supported, it depends on the defaultAddress + return {} + } + + access(all) view fun isSupportedVaultType(type: Type): Bool { + // theoretically any token is supported, it depends on the defaultAddress + return true + } + + init(defaultAddress: Address) { + self.defaultAddress = defaultAddress + self.addressOverrides = {} + + emit RouterCreated(uuid: self.uuid, defaultAddress: defaultAddress) + } + } + + access(all) fun createRouter(defaultAddress: Address): @Router { + return <- create Router(defaultAddress: defaultAddress) + } + + init() { + let identifier = "FungibleTokenRouter_".concat(self.account.address.toString()) + self.StoragePath = StoragePath(identifier: identifier)! + self.PublicPath = PublicPath(identifier: identifier)! + } +} \ No newline at end of file diff --git a/contracts/hybrid-custody/CapabilityDelegator.cdc b/contracts/hybrid-custody/CapabilityDelegator.cdc index f342aaf..796002b 100644 --- a/contracts/hybrid-custody/CapabilityDelegator.cdc +++ b/contracts/hybrid-custody/CapabilityDelegator.cdc @@ -7,48 +7,51 @@ /// private `Delegator` can only be borrowed from the child account when you have access to the full `ChildAccount` /// resource. /// -pub contract CapabilityDelegator { +access(all) contract CapabilityDelegator { /* --- Canonical Paths --- */ // - pub let StoragePath: StoragePath - pub let PrivatePath: PrivatePath - pub let PublicPath: PublicPath + access(all) let StoragePath: StoragePath + access(all) let PublicPath: PublicPath + + access(all) entitlement Get + access(all) entitlement Add + access(all) entitlement Delete /* --- Events --- */ // - pub event DelegatorCreated(id: UInt64) - pub event DelegatorUpdated(id: UInt64, capabilityType: Type, isPublic: Bool, active: Bool) + access(all) event DelegatorCreated(id: UInt64) + access(all) event DelegatorUpdated(id: UInt64, capabilityType: Type, isPublic: Bool, active: Bool) /// Private interface for Capability retrieval /// - pub resource interface GetterPrivate { - pub fun getPrivateCapability(_ type: Type): Capability? { + access(all) resource interface GetterPrivate { + access(Get) view fun getPrivateCapability(_ type: Type): Capability? { post { result == nil || type.isSubtype(of: result.getType()): "incorrect returned capability type" } } - pub fun findFirstPrivateType(_ type: Type): Type? - pub fun getAllPrivate(): [Capability] + access(all) view fun findFirstPrivateType(_ type: Type): Type? + access(Get) fun getAllPrivate(): [Capability] } /// Exposes public Capability retrieval /// - pub resource interface GetterPublic { - pub fun getPublicCapability(_ type: Type): Capability? { + access(all) resource interface GetterPublic { + access(all) view fun getPublicCapability(_ type: Type): Capability? { post { - result == nil || type.isSubtype(of: result.getType()): "incorrect returned capability type " + result == nil || type.isSubtype(of: result.getType()): "incorrect returned capability type" } } - pub fun findFirstPublicType(_ type: Type): Type? - pub fun getAllPublic(): [Capability] + access(all) view fun findFirstPublicType(_ type: Type): Type? + access(all) view fun getAllPublic(): [Capability] } /// This Delegator is used to store Capabilities, partitioned by public and private access with corresponding /// GetterPublic and GetterPrivate conformances.AccountCapabilityController /// - pub resource Delegator: GetterPublic, GetterPrivate { + access(all) resource Delegator: GetterPublic, GetterPrivate { access(self) let privateCapabilities: {Type: Capability} access(self) let publicCapabilities: {Type: Capability} @@ -56,7 +59,7 @@ pub contract CapabilityDelegator { // /// Returns the public Capability of the given Type if it exists /// - pub fun getPublicCapability(_ type: Type): Capability? { + access(all) view fun getPublicCapability(_ type: Type): Capability? { return self.publicCapabilities[type] } @@ -66,7 +69,7 @@ pub contract CapabilityDelegator { /// @param type: Type of the Capability to retrieve /// @return Capability of the given Type if it exists, nil otherwise /// - pub fun getPrivateCapability(_ type: Type): Capability? { + access(Get) view fun getPrivateCapability(_ type: Type): Capability? { return self.privateCapabilities[type] } @@ -74,7 +77,7 @@ pub contract CapabilityDelegator { /// /// @return List of all public Capabilities /// - pub fun getAllPublic(): [Capability] { + access(all) view fun getAllPublic(): [Capability] { return self.publicCapabilities.values } @@ -82,7 +85,7 @@ pub contract CapabilityDelegator { /// /// @return List of all private Capabilities /// - pub fun getAllPrivate(): [Capability] { + access(Get) fun getAllPrivate(): [Capability] { return self.privateCapabilities.values } @@ -91,7 +94,7 @@ pub contract CapabilityDelegator { /// @param type: Type to check for subtypes /// @return First public Type that is a subtype of the given Type, nil otherwise /// - pub fun findFirstPublicType(_ type: Type): Type? { + access(all) view fun findFirstPublicType(_ type: Type): Type? { for t in self.publicCapabilities.keys { if t.isSubtype(of: type) { return t @@ -106,7 +109,7 @@ pub contract CapabilityDelegator { /// @param type: Type to check for subtypes /// @return First private Type that is a subtype of the given Type, nil otherwise /// - pub fun findFirstPrivateType(_ type: Type): Type? { + access(all) view fun findFirstPrivateType(_ type: Type): Type? { for t in self.privateCapabilities.keys { if t.isSubtype(of: type) { return t @@ -122,7 +125,7 @@ pub contract CapabilityDelegator { /// @param cap: Capability to add /// @param isPublic: Whether the Capability should be public or private /// - pub fun addCapability(cap: Capability, isPublic: Bool) { + access(Add) fun addCapability(cap: Capability, isPublic: Bool) { pre { cap.check<&AnyResource>(): "Invalid Capability provided" } @@ -138,7 +141,7 @@ pub contract CapabilityDelegator { /// /// @param cap: Capability to remove /// - pub fun removeCapability(cap: Capability) { + access(Delete) fun removeCapability(cap: Capability) { if let removedPublic = self.publicCapabilities.remove(key: cap.getType()) { emit DelegatorUpdated(id: self.uuid, capabilityType: cap.getType(), isPublic: true, active: false) } @@ -158,7 +161,7 @@ pub contract CapabilityDelegator { /// /// @return Newly created Delegator /// - pub fun createDelegator(): @Delegator { + access(all) fun createDelegator(): @Delegator { let delegator <- create Delegator() emit DelegatorCreated(id: delegator.uuid) return <- delegator @@ -167,7 +170,6 @@ pub contract CapabilityDelegator { init() { let identifier = "CapabilityDelegator_".concat(self.account.address.toString()) self.StoragePath = StoragePath(identifier: identifier)! - self.PrivatePath = PrivatePath(identifier: identifier)! self.PublicPath = PublicPath(identifier: identifier)! } } diff --git a/contracts/hybrid-custody/CapabilityFactory.cdc b/contracts/hybrid-custody/CapabilityFactory.cdc index ee777f4..f4b62ac 100644 --- a/contracts/hybrid-custody/CapabilityFactory.cdc +++ b/contracts/hybrid-custody/CapabilityFactory.cdc @@ -13,37 +13,40 @@ /// Capabilities is critical to the use case of Hybrid Custody. It's advised to use Factories sparingly and only for /// cases where Capabilities must be castable by the caller. /// -pub contract CapabilityFactory { +access(all) contract CapabilityFactory { - pub let StoragePath: StoragePath - pub let PrivatePath: PrivatePath - pub let PublicPath: PublicPath + access(all) let StoragePath: StoragePath + access(all) let PublicPath: PublicPath + + access(all) entitlement Add + access(all) entitlement Delete /// Factory structures a common interface for Capability retrieval from a given account at a specified path /// - pub struct interface Factory { - pub fun getCapability(acct: &AuthAccount, path: CapabilityPath): Capability + access(all) struct interface Factory { + access(all) view fun getCapability(acct: auth(Capabilities) &Account, controllerID: UInt64): Capability? + access(all) view fun getPublicCapability(acct: &Account, path: PublicPath): Capability? } /// Getter defines an interface for retrieval of a Factory if contained within the implementing resource /// - pub resource interface Getter { - pub fun getSupportedTypes(): [Type] - pub fun getFactory(_ t: Type): {CapabilityFactory.Factory}? + access(all) resource interface Getter { + access(all) view fun getSupportedTypes(): [Type] + access(all) view fun getFactory(_ t: Type): {CapabilityFactory.Factory}? } /// Manager is a resource that contains Factories and implements the Getter interface for retrieval of contained /// Factories /// - pub resource Manager: Getter { + access(all) resource Manager: Getter { /// Mapping of Factories indexed on Type of Capability they retrieve - pub let factories: {Type: {CapabilityFactory.Factory}} + access(all) let factories: {Type: {CapabilityFactory.Factory}} /// Retrieves a list of Types supported by contained Factories /// /// @return List of Types supported by the Manager /// - pub fun getSupportedTypes(): [Type] { + access(all) view fun getSupportedTypes(): [Type] { return self.factories.keys } @@ -51,7 +54,7 @@ pub contract CapabilityFactory { /// /// @param t: Type the Factory is indexed on /// - pub fun getFactory(_ t: Type): {CapabilityFactory.Factory}? { + access(all) view fun getFactory(_ t: Type): {CapabilityFactory.Factory}? { return self.factories[t] } @@ -60,7 +63,7 @@ pub contract CapabilityFactory { /// @param t: Type of Capability the Factory retrieves /// @param f: Factory to add /// - pub fun addFactory(_ t: Type, _ f: {CapabilityFactory.Factory}) { + access(Add) fun addFactory(_ t: Type, _ f: {CapabilityFactory.Factory}) { pre { !self.factories.containsKey(t): "Factory of given type already exists" } @@ -72,7 +75,7 @@ pub contract CapabilityFactory { /// @param t: Type of Capability the Factory retrieves /// @param f: Factory to replace existing Factory /// - pub fun updateFactory(_ t: Type, _ f: {CapabilityFactory.Factory}) { + access(Add) fun updateFactory(_ t: Type, _ f: {CapabilityFactory.Factory}) { self.factories[t] = f } @@ -80,7 +83,7 @@ pub contract CapabilityFactory { /// /// @param t: Type the Factory is indexed on /// - pub fun removeFactory(_ t: Type): {CapabilityFactory.Factory}? { + access(Delete) fun removeFactory(_ t: Type): {CapabilityFactory.Factory}? { return self.factories.remove(key: t) } @@ -92,14 +95,13 @@ pub contract CapabilityFactory { /// Creates a Manager resource /// /// @return Manager resource - pub fun createFactoryManager(): @Manager { + access(all) fun createFactoryManager(): @Manager { return <- create Manager() } init() { let identifier = "CapabilityFactory_".concat(self.account.address.toString()) self.StoragePath = StoragePath(identifier: identifier)! - self.PrivatePath = PrivatePath(identifier: identifier)! self.PublicPath = PublicPath(identifier: identifier)! } } \ No newline at end of file diff --git a/contracts/hybrid-custody/CapabilityFilter.cdc b/contracts/hybrid-custody/CapabilityFilter.cdc index fae1d02..efd8487 100644 --- a/contracts/hybrid-custody/CapabilityFilter.cdc +++ b/contracts/hybrid-custody/CapabilityFilter.cdc @@ -6,29 +6,31 @@ /// - `AllowlistFilter` - A filter which contains a mapping of allowed Types /// - `AllowAllFilter` - A passthrough, all requested capabilities are allowed /// -pub contract CapabilityFilter { +access(all) contract CapabilityFilter { /* --- Canonical Paths --- */ // - pub let StoragePath: StoragePath - pub let PublicPath: PublicPath - pub let PrivatePath: PrivatePath + access(all) let StoragePath: StoragePath + access(all) let PublicPath: PublicPath + + access(all) entitlement Add + access(all) entitlement Delete /* --- Events --- */ // - pub event FilterUpdated(id: UInt64, filterType: Type, type: Type, active: Bool) + access(all) event FilterUpdated(id: UInt64, filterType: Type, type: Type, active: Bool) /// `Filter` is a simple interface with methods to determine if a Capability is allowed and retrieve details about /// the Filter itself /// - pub resource interface Filter { - pub fun allowed(cap: Capability): Bool - pub fun getDetails(): AnyStruct + access(all) resource interface Filter { + access(all) view fun allowed(cap: Capability): Bool + access(all) view fun getDetails(): AnyStruct } /// `DenylistFilter` is a `Filter` which contains a mapping of denied Types /// - pub resource DenylistFilter: Filter { + access(all) resource DenylistFilter: Filter { /// Represents the underlying types which should not ever be returned by a RestrictedChildAccount. The filter /// will borrow a requested capability, and make sure that the type it gets back is not in the list of denied @@ -39,7 +41,7 @@ pub contract CapabilityFilter { /// /// @param type: The type to add to the denied types mapping /// - pub fun addType(_ type: Type) { + access(Add) fun addType(_ type: Type) { self.deniedTypes.insert(key: type, true) emit FilterUpdated(id: self.uuid, filterType: self.getType(), type: type, active: true) } @@ -48,18 +50,26 @@ pub contract CapabilityFilter { /// /// @param type: The type to remove from the denied types mapping /// - pub fun removeType(_ type: Type) { + access(Delete) fun removeType(_ type: Type) { if let removed = self.deniedTypes.remove(key: type) { emit FilterUpdated(id: self.uuid, filterType: self.getType(), type: type, active: false) } } + /// Removes all types from the mapping of denied types + /// + access(Delete) fun removeAllTypes() { + for type in self.deniedTypes.keys { + self.removeType(type) + } + } + /// Determines if a requested capability is allowed by this `Filter` /// /// @param cap: The capability to check /// @return: true if the capability is allowed, false otherwise /// - pub fun allowed(cap: Capability): Bool { + access(all) view fun allowed(cap: Capability): Bool { if let item = cap.borrow<&AnyResource>() { return !self.deniedTypes.containsKey(item.getType()) } @@ -72,7 +82,7 @@ pub contract CapabilityFilter { /// @return A struct containing details about this filter including this Filter's Type indexed on the `type` /// key as well as types denied indexed on the `deniedTypes` key /// - pub fun getDetails(): AnyStruct { + access(all) view fun getDetails(): AnyStruct { return { "type": self.getType(), "deniedTypes": self.deniedTypes.keys @@ -86,7 +96,7 @@ pub contract CapabilityFilter { /// `AllowlistFilter` is a `Filter` which contains a mapping of allowed Types /// - pub resource AllowlistFilter: Filter { + access(all) resource AllowlistFilter: Filter { // allowedTypes // Represents the set of underlying types which are allowed to be // returned by a RestrictedChildAccount. The filter will borrow @@ -98,7 +108,7 @@ pub contract CapabilityFilter { /// /// @param type: The type to add to the allowed types mapping /// - pub fun addType(_ type: Type) { + access(Add) fun addType(_ type: Type) { self.allowedTypes.insert(key: type, true) emit FilterUpdated(id: self.uuid, filterType: self.getType(), type: type, active: true) } @@ -107,18 +117,26 @@ pub contract CapabilityFilter { /// /// @param type: The type to remove from the denied types mapping /// - pub fun removeType(_ type: Type) { + access(Delete) fun removeType(_ type: Type) { if let removed = self.allowedTypes.remove(key: type) { emit FilterUpdated(id: self.uuid, filterType: self.getType(), type: type, active: false) } } + + /// Removes all types from the mapping of denied types + /// + access(Delete) fun removeAllTypes() { + for type in self.allowedTypes.keys { + self.removeType(type) + } + } /// Determines if a requested capability is allowed by this `Filter` /// /// @param cap: The capability to check /// @return: true if the capability is allowed, false otherwise /// - pub fun allowed(cap: Capability): Bool { + access(all) view fun allowed(cap: Capability): Bool { if let item = cap.borrow<&AnyResource>() { return self.allowedTypes.containsKey(item.getType()) } @@ -131,7 +149,7 @@ pub contract CapabilityFilter { /// @return A struct containing details about this filter including this Filter's Type indexed on the `type` /// key as well as types allowed indexed on the `allowedTypes` key /// - pub fun getDetails(): AnyStruct { + access(all) view fun getDetails(): AnyStruct { return { "type": self.getType(), "allowedTypes": self.allowedTypes.keys @@ -145,13 +163,13 @@ pub contract CapabilityFilter { /// AllowAllFilter is a passthrough, all requested capabilities are allowed /// - pub resource AllowAllFilter: Filter { + access(all) resource AllowAllFilter: Filter { /// Determines if a requested capability is allowed by this `Filter` /// /// @param cap: The capability to check /// @return: true since this filter is a passthrough /// - pub fun allowed(cap: Capability): Bool { + access(all) view fun allowed(cap: Capability): Bool { return true } @@ -160,7 +178,7 @@ pub contract CapabilityFilter { /// @return A struct containing details about this filter including this Filter's Type indexed on the `type` /// key /// - pub fun getDetails(): AnyStruct { + access(all) view fun getDetails(): AnyStruct { return { "type": self.getType() } @@ -172,7 +190,7 @@ pub contract CapabilityFilter { /// @param t: The type of `Filter` to create /// @return: A new instance of the given `Filter` type /// - pub fun create(_ t: Type): @AnyResource{Filter} { + access(all) fun createFilter(_ t: Type): @{Filter} { post { result.getType() == t } @@ -194,6 +212,5 @@ pub contract CapabilityFilter { self.StoragePath = StoragePath(identifier: identifier)! self.PublicPath = PublicPath(identifier: identifier)! - self.PrivatePath = PrivatePath(identifier: identifier)! } -} \ No newline at end of file +} diff --git a/contracts/hybrid-custody/HybridCustody.cdc b/contracts/hybrid-custody/HybridCustody.cdc index 93806d9..7602830 100644 --- a/contracts/hybrid-custody/HybridCustody.cdc +++ b/contracts/hybrid-custody/HybridCustody.cdc @@ -1,5 +1,7 @@ // Third-party imports import "MetadataViews" +import "ViewResolver" +import "Burner" // HC-owned imports import "CapabilityFactory" @@ -27,39 +29,37 @@ import "CapabilityFilter" /// /// Repo reference: https://github.com/onflow/hybrid-custody /// -pub contract HybridCustody { +access(all) contract HybridCustody { + access(all) entitlement Owner + access(all) entitlement Child + access(all) entitlement Manage /* --- Canonical Paths --- */ // // Note: Paths for ChildAccount & Delegator are derived from the parent's address // - pub let OwnedAccountStoragePath: StoragePath - pub let OwnedAccountPublicPath: PublicPath - pub let OwnedAccountPrivatePath: PrivatePath + access(all) let OwnedAccountStoragePath: StoragePath + access(all) let OwnedAccountPublicPath: PublicPath - pub let ManagerStoragePath: StoragePath - pub let ManagerPublicPath: PublicPath - pub let ManagerPrivatePath: PrivatePath - - pub let LinkedAccountPrivatePath: PrivatePath - pub let BorrowableAccountPrivatePath: PrivatePath + access(all) let ManagerStoragePath: StoragePath + access(all) let ManagerPublicPath: PublicPath /* --- Events --- */ // /// Manager creation event - pub event CreatedManager(id: UInt64) + access(all) event CreatedManager(id: UInt64) /// OwnedAccount creation event - pub event CreatedOwnedAccount(id: UInt64, child: Address) + access(all) event CreatedOwnedAccount(id: UInt64, child: Address) /// ChildAccount added/removed from Manager /// active : added to Manager /// !active : removed from Manager - pub event AccountUpdated(id: UInt64?, child: Address, parent: Address, active: Bool) + access(all) event AccountUpdated(id: UInt64?, child: Address, parent: Address?, active: Bool) /// OwnedAccount added/removed or sealed /// active && owner != nil : added to Manager /// !active && owner == nil : removed from Manager - pub event OwnershipUpdated(id: UInt64, child: Address, previousOwner: Address?, owner: Address?, active: Bool) + access(all) event OwnershipUpdated(id: UInt64, child: Address, previousOwner: Address?, owner: Address?, active: Bool) /// ChildAccount ready to be redeemed by emitted pendingParent - pub event ChildAccountPublished( + access(all) event ChildAccountPublished( ownedAcctID: UInt64, childAcctID: UInt64, capDelegatorID: UInt64, @@ -70,49 +70,52 @@ pub contract HybridCustody { pendingParent: Address ) /// OwnedAccount granted ownership to a new address, publishing a Capability for the pendingOwner - pub event OwnershipGranted(ownedAcctID: UInt64, child: Address, previousOwner: Address?, pendingOwner: Address) + access(all) event OwnershipGranted(ownedAcctID: UInt64, child: Address, previousOwner: Address?, pendingOwner: Address) /// Account has been sealed - keys revoked, new AuthAccount Capability generated - pub event AccountSealed(id: UInt64, address: Address, parents: [Address]) + access(all) event AccountSealed(id: UInt64, address: Address, parents: [Address]) /// An OwnedAccount shares the BorrowableAccount capability to itelf with ChildAccount resources /// - pub resource interface BorrowableAccount { - access(contract) fun borrowAccount(): &AuthAccount - pub fun check(): Bool + access(all) resource interface BorrowableAccount { + access(contract) view fun _borrowAccount(): auth(Storage, Contracts, Keys, Inbox, Capabilities) &Account + access(all) view fun check(): Bool } /// Public methods anyone can call on an OwnedAccount /// - pub resource interface OwnedAccountPublic { + access(all) resource interface OwnedAccountPublic { /// Returns the addresses of all parent accounts - pub fun getParentAddresses(): [Address] + access(all) view fun getParentAddresses(): [Address] /// Returns associated parent addresses and their redeemed status - true if redeemed, false if pending - pub fun getParentStatuses(): {Address: Bool} + access(all) view fun getParentStatuses(): {Address: Bool} /// Returns true if the given address is a parent of this child and has redeemed it. Returns false if the given /// address is a parent of this child and has NOT redeemed it. Returns nil if the given address it not a parent /// of this child account. - pub fun getRedeemedStatus(addr: Address): Bool? + access(all) view fun getRedeemedStatus(addr: Address): Bool? /// A callback function to mark a parent as redeemed on the child account. access(contract) fun setRedeemed(_ addr: Address) + + /// A helper function to find what controller Id to ask for if you are looking for a specific type of capability + access(all) view fun getControllerIDForType(type: Type, forPath: StoragePath): UInt64? } /// Private interface accessible to the owner of the OwnedAccount /// - pub resource interface OwnedAccountPrivate { + access(all) resource interface OwnedAccountPrivate { /// Deletes the ChildAccount resource being used to share access to this OwnedAccount with the supplied parent /// address, and unlinks the paths it was using to reach the underlying account. - pub fun removeParent(parent: Address): Bool + access(Owner) fun removeParent(parent: Address): Bool /// Sets up a new ChildAccount resource for the given parentAddress to redeem. This child account uses the /// supplied factory and filter to manage what can be obtained from the child account, and a new /// CapabilityDelegator resource is created for the sharing of one-off capabilities. Each of these pieces of /// access control are managed through the child account. - pub fun publishToParent( + access(Owner) fun publishToParent( parentAddress: Address, - factory: Capability<&CapabilityFactory.Manager{CapabilityFactory.Getter}>, + factory: Capability<&CapabilityFactory.Manager>, filter: Capability<&{CapabilityFilter.Filter}> ) { pre { @@ -124,7 +127,7 @@ pub contract HybridCustody { /// Passes ownership of this child account to the given address. Once executed, all active keys on the child /// account will be revoked, and the active AuthAccount Capability being used by to obtain capabilities will be /// rotated, preventing anyone without the newly generated Capability from gaining access to the account. - pub fun giveOwnership(to: Address) + access(Owner) fun giveOwnership(to: Address) /// Revokes all keys on an account, unlinks all currently active AuthAccount capabilities, then makes a new one /// and replaces the OwnedAccount's underlying AuthAccount Capability with the new one to ensure that all @@ -132,20 +135,20 @@ pub contract HybridCustody { /// Unless this method is executed via the giveOwnership function, this will leave an account **without** an /// owner. /// USE WITH EXTREME CAUTION. - pub fun seal() + access(Owner) fun seal() // setCapabilityFactoryForParent // Override the existing CapabilityFactory Capability for a given parent. This will allow the owner of the // account to start managing their own factory of capabilities to be able to retrieve - pub fun setCapabilityFactoryForParent(parent: Address, cap: Capability<&CapabilityFactory.Manager{CapabilityFactory.Getter}>) { + access(Owner) fun setCapabilityFactoryForParent(parent: Address, cap: Capability<&CapabilityFactory.Manager>) { pre { cap.check(): "Invalid CapabilityFactory.Getter Capability provided" } } /// Override the existing CapabilityFilter Capability for a given parent. This will allow the owner of the - /// account to start managing their own filter for retrieving Capabilities on Private Paths - pub fun setCapabilityFilterForParent(parent: Address, cap: Capability<&{CapabilityFilter.Filter}>) { + /// account to start managing their own filter for retrieving Capabilities + access(Owner) fun setCapabilityFilterForParent(parent: Address, cap: Capability<&{CapabilityFilter.Filter}>) { pre { cap.check(): "Invalid CapabilityFilter Capability provided" } @@ -153,66 +156,67 @@ pub contract HybridCustody { /// Adds a capability to a parent's managed @ChildAccount resource. The Capability can be made public, /// permitting anyone to borrow it. - pub fun addCapabilityToDelegator(parent: Address, cap: Capability, isPublic: Bool) { + access(Owner) fun addCapabilityToDelegator(parent: Address, cap: Capability, isPublic: Bool) { pre { cap.check<&AnyResource>(): "Invalid Capability provided" } } /// Removes a Capability from the CapabilityDelegator used by the specified parent address - pub fun removeCapabilityFromDelegator(parent: Address, cap: Capability) + access(Owner) fun removeCapabilityFromDelegator(parent: Address, cap: Capability) /// Returns the address of this OwnedAccount - pub fun getAddress(): Address + access(all) view fun getAddress(): Address /// Checks if this OwnedAccount is a child of the specified address - pub fun isChildOf(_ addr: Address): Bool + access(all) view fun isChildOf(_ addr: Address): Bool /// Returns all addresses which are parents of this OwnedAccount - pub fun getParentAddresses(): [Address] + access(all) view fun getParentAddresses(): [Address] /// Borrows this OwnedAccount's AuthAccount Capability - pub fun borrowAccount(): &AuthAccount? + access(Owner) view fun borrowAccount(): auth(Storage, Contracts, Keys, Inbox, Capabilities) &Account /// Returns the current owner of this account, if there is one - pub fun getOwner(): Address? + access(all) view fun getOwner(): Address? /// Returns the pending owner of this account, if there is one - pub fun getPendingOwner(): Address? + access(all) view fun getPendingOwner(): Address? /// A callback which is invoked when a parent redeems an owned account access(contract) fun setOwnerCallback(_ addr: Address) /// Destroys all outstanding AuthAccount capabilities on this owned account, and creates a new one for the /// OwnedAccount to use - pub fun rotateAuthAccount() + access(Owner) fun rotateAuthAccount() /// Revokes all keys on this account - pub fun revokeAllKeys() + access(Owner) fun revokeAllKeys() } /// Public methods exposed on a ChildAccount resource. OwnedAccountPublic will share some methods here, but isn't /// necessarily the same. /// - pub resource interface AccountPublic { - pub fun getPublicCapability(path: PublicPath, type: Type): Capability? - pub fun getPublicCapFromDelegator(type: Type): Capability? - pub fun getAddress(): Address + access(all) resource interface AccountPublic { + access(all) view fun getPublicCapability(path: PublicPath, type: Type): Capability? + access(all) view fun getPublicCapFromDelegator(type: Type): Capability? + access(all) view fun getAddress(): Address + access(all) view fun getCapabilityFactoryManager(): &{CapabilityFactory.Getter}? + access(all) view fun getCapabilityFilter(): &{CapabilityFilter.Filter}? + access(all) view fun getControllerIDForType(type: Type, forPath: StoragePath): UInt64? } /// Methods accessible to the designated parent of a ChildAccount /// - pub resource interface AccountPrivate { - pub fun getCapability(path: CapabilityPath, type: Type): Capability? { + access(all) resource interface AccountPrivate { + access(Child) view fun getCapability(controllerID: UInt64, type: Type): Capability? { post { result == nil || [true, nil].contains(self.getManagerCapabilityFilter()?.allowed(cap: result!)): "Capability is not allowed by this account's Parent" } } - pub fun getPublicCapability(path: PublicPath, type: Type): Capability? - pub fun getManagerCapabilityFilter(): &{CapabilityFilter.Filter}? - pub fun getPublicCapFromDelegator(type: Type): Capability? - pub fun getPrivateCapFromDelegator(type: Type): Capability? { + access(all) view fun getManagerCapabilityFilter(): &{CapabilityFilter.Filter}? + access(Child) view fun getPrivateCapFromDelegator(type: Type): Capability? { post { result == nil || [true, nil].contains(self.getManagerCapabilityFilter()?.allowed(cap: result!)): "Capability is not allowed by this account's Parent" @@ -229,14 +233,14 @@ pub contract HybridCustody { /// Entry point for a parent to obtain, maintain and access Capabilities or perform other actions on child accounts /// - pub resource interface ManagerPrivate { - pub fun addAccount(cap: Capability<&{AccountPrivate, AccountPublic, MetadataViews.Resolver}>) - pub fun borrowAccount(addr: Address): &{AccountPrivate, AccountPublic, MetadataViews.Resolver}? - pub fun removeChild(addr: Address) - pub fun addOwnedAccount(cap: Capability<&{OwnedAccountPrivate, OwnedAccountPublic, MetadataViews.Resolver}>) - pub fun borrowOwnedAccount(addr: Address): &{OwnedAccountPrivate, OwnedAccountPublic, MetadataViews.Resolver}? - pub fun removeOwned(addr: Address) - pub fun setManagerCapabilityFilter(cap: Capability<&{CapabilityFilter.Filter}>?, childAddress: Address) { + access(all) resource interface ManagerPrivate { + access(Manage) fun addAccount(cap: Capability) + access(Manage) fun borrowAccount(addr: Address): auth(Child) &{AccountPrivate, AccountPublic, ViewResolver.Resolver}? + access(Manage) fun removeChild(addr: Address) + access(Manage) fun addOwnedAccount(cap: Capability) + access(Manage) fun borrowOwnedAccount(addr: Address): auth(Owner) &{OwnedAccountPrivate, OwnedAccountPublic, ViewResolver.Resolver}? + access(Manage) fun removeOwned(addr: Address) + access(Manage) fun setManagerCapabilityFilter(cap: Capability<&{CapabilityFilter.Filter}>?, childAddress: Address) { pre { cap == nil || cap!.check(): "Invalid Manager Capability Filter" } @@ -245,39 +249,40 @@ pub contract HybridCustody { /// Functions anyone can call on a manager to get information about an account such as What child accounts it has /// Functions anyone can call on a manager to get information about an account such as what child accounts it has - pub resource interface ManagerPublic { - pub fun borrowAccountPublic(addr: Address): &{AccountPublic, MetadataViews.Resolver}? - pub fun getChildAddresses(): [Address] - pub fun getOwnedAddresses(): [Address] - pub fun getChildAccountDisplay(address: Address): MetadataViews.Display? + access(all) resource interface ManagerPublic { + access(all) view fun borrowAccountPublic(addr: Address): &{AccountPublic, ViewResolver.Resolver}? + access(all) view fun getChildAddresses(): [Address] + access(all) view fun getOwnedAddresses(): [Address] + access(all) view fun getChildAccountDisplay(address: Address): MetadataViews.Display? access(contract) fun removeParentCallback(child: Address) } /// A resource for an account which fills the Parent role of the Child-Parent account management Model. A Manager /// can redeem or remove child accounts, and obtain any capabilities exposed by the child account to them. /// - pub resource Manager: ManagerPrivate, ManagerPublic, MetadataViews.Resolver { + access(all) resource Manager: ManagerPrivate, ManagerPublic, ViewResolver.Resolver, Burner.Burnable { + access(all) event ResourceDestroyed(uuid: UInt64 = self.uuid) /// Mapping of restricted access child account Capabilities indexed by their address - pub let childAccounts: {Address: Capability<&{AccountPrivate, AccountPublic, MetadataViews.Resolver}>} + access(self) let childAccounts: {Address: Capability} /// Mapping of unrestricted owned account Capabilities indexed by their address - pub let ownedAccounts: {Address: Capability<&{OwnedAccountPrivate, OwnedAccountPublic, MetadataViews.Resolver}>} + access(self) let ownedAccounts: {Address: Capability} /// A bucket of structs so that the Manager resource can be easily extended with new functionality. - pub let data: {String: AnyStruct} + access(self) let data: {String: AnyStruct} /// A bucket of resources so that the Manager resource can be easily extended with new functionality. - pub let resources: @{String: AnyResource} + access(self) let resources: @{String: AnyResource} /// An optional filter to gate what capabilities are permitted to be returned from a child account For example, /// Dapper Wallet parent account's should not be able to retrieve any FungibleToken Provider capabilities. - pub var filter: Capability<&{CapabilityFilter.Filter}>? + access(self) var filter: Capability<&{CapabilityFilter.Filter}>? // display metadata for a child account exists on its parent - pub let childAccountDisplays: {Address: MetadataViews.Display} + access(self) let childAccountDisplays: {Address: MetadataViews.Display} /// Sets the Display on the ChildAccount. If nil, the display is removed. /// - pub fun setChildAccountDisplay(address: Address, _ d: MetadataViews.Display?) { + access(Manage) fun setChildAccountDisplay(address: Address, _ d: MetadataViews.Display?) { pre { self.childAccounts[address] != nil: "There is no child account with this address" } @@ -293,7 +298,7 @@ pub contract HybridCustody { /// Adds a ChildAccount Capability to this Manager. If a default Filter is set in the manager, it will also be /// added to the ChildAccount /// - pub fun addAccount(cap: Capability<&{AccountPrivate, AccountPublic, MetadataViews.Resolver}>) { + access(Manage) fun addAccount(cap: Capability) { pre { self.childAccounts[cap.address] == nil: "There is already a child account with this address" } @@ -311,17 +316,17 @@ pub contract HybridCustody { /// Sets the default Filter Capability for this Manager. Does not propagate to child accounts. /// - pub fun setDefaultManagerCapabilityFilter(cap: Capability<&{CapabilityFilter.Filter}>?) { + access(Manage) fun setDefaultManagerCapabilityFilter(cap: Capability<&{CapabilityFilter.Filter}>?) { pre { cap == nil || cap!.check(): "supplied capability must be nil or check must pass" } self.filter = cap } - + /// Sets the Filter Capability for this Manager, propagating to the specified child account /// - pub fun setManagerCapabilityFilter(cap: Capability<&{CapabilityFilter.Filter}>?, childAddress: Address) { + access(Manage) fun setManagerCapabilityFilter(cap: Capability<&{CapabilityFilter.Filter}>?, childAddress: Address) { let acct = self.borrowAccount(addr: childAddress) ?? panic("child account not found") @@ -331,7 +336,7 @@ pub contract HybridCustody { /// Removes specified child account from the Manager's child accounts. Callbacks to the child account remove /// any associated resources and Capabilities /// - pub fun removeChild(addr: Address) { + access(Manage) fun removeChild(addr: Address) { let cap = self.childAccounts.remove(key: addr) ?? panic("child account not found") @@ -347,9 +352,11 @@ pub contract HybridCustody { // Get the child account id before removing capability let id: UInt64 = acct.uuid - acct.parentRemoveChildCallback(parent: self.owner!.address) + if self.owner != nil { + acct.parentRemoveChildCallback(parent: self.owner!.address) + } - emit AccountUpdated(id: id, child: cap.address, parent: self.owner!.address, active: false) + emit AccountUpdated(id: id, child: cap.address, parent: self.owner?.address, active: false) } /// Contract callback that removes a child account from the Manager's child accounts in the event a child @@ -363,7 +370,7 @@ pub contract HybridCustody { /// Adds an owned account to the Manager's list of owned accounts, setting the Manager account as the owner of /// the given account /// - pub fun addOwnedAccount(cap: Capability<&{OwnedAccountPrivate, OwnedAccountPublic, MetadataViews.Resolver}>) { + access(Manage) fun addOwnedAccount(cap: Capability) { pre { self.ownedAccounts[cap.address] == nil: "There is already an owned account with this address" } @@ -384,7 +391,7 @@ pub contract HybridCustody { /// Returns a reference to a child account /// - pub fun borrowAccount(addr: Address): &{AccountPrivate, AccountPublic, MetadataViews.Resolver}? { + access(Manage) fun borrowAccount(addr: Address): auth(Child) &{AccountPrivate, AccountPublic, ViewResolver.Resolver}? { let cap = self.childAccounts[addr] if cap == nil { return nil @@ -395,7 +402,7 @@ pub contract HybridCustody { /// Returns a reference to a child account's public AccountPublic interface /// - pub fun borrowAccountPublic(addr: Address): &{AccountPublic, MetadataViews.Resolver}? { + access(all) view fun borrowAccountPublic(addr: Address): &{AccountPublic, ViewResolver.Resolver}? { let cap = self.childAccounts[addr] if cap == nil { return nil @@ -406,7 +413,7 @@ pub contract HybridCustody { /// Returns a reference to an owned account /// - pub fun borrowOwnedAccount(addr: Address): &{OwnedAccountPrivate, OwnedAccountPublic, MetadataViews.Resolver}? { + access(Manage) view fun borrowOwnedAccount(addr: Address): auth(Owner) &{OwnedAccountPrivate, OwnedAccountPublic, ViewResolver.Resolver}? { if let cap = self.ownedAccounts[addr] { return cap.borrow() } @@ -417,7 +424,7 @@ pub contract HybridCustody { /// Removes specified child account from the Manager's child accounts. Callbacks to the child account remove /// any associated resources and Capabilities /// - pub fun removeOwned(addr: Address) { + access(Manage) fun removeOwned(addr: Address) { if let acct = self.ownedAccounts.remove(key: addr) { if acct.check() { acct.borrow()!.seal() @@ -437,7 +444,7 @@ pub contract HybridCustody { /// mechanism intended to easily transfer 'root' access on this account to another account and an attempt to /// minimize access vectors. /// - pub fun giveOwnership(addr: Address, to: Address) { + access(Manage) fun giveOwnership(addr: Address, to: Address) { let acct = self.ownedAccounts.remove(key: addr) ?? panic("account not found") @@ -446,31 +453,31 @@ pub contract HybridCustody { /// Returns an array of child account addresses /// - pub fun getChildAddresses(): [Address] { + access(all) view fun getChildAddresses(): [Address] { return self.childAccounts.keys } /// Returns an array of owned account addresses /// - pub fun getOwnedAddresses(): [Address] { + access(all) view fun getOwnedAddresses(): [Address] { return self.ownedAccounts.keys } /// Retrieves the parent-defined display for the given child account /// - pub fun getChildAccountDisplay(address: Address): MetadataViews.Display? { + access(all) view fun getChildAccountDisplay(address: Address): MetadataViews.Display? { return self.childAccountDisplays[address] } /// Returns the types of supported views - none at this time /// - pub fun getViews(): [Type] { + access(all) view fun getViews(): [Type] { return [] } /// Resolves the given view if supported - none at this time /// - pub fun resolveView(_ view: Type): AnyStruct? { + access(all) view fun resolveView(_ view: Type): AnyStruct? { return nil } @@ -487,8 +494,19 @@ pub contract HybridCustody { self.resources <- {} } - destroy () { - destroy self.resources + // When a manager resource is destroyed, attempt to remove this parent from every + // child account it currently has + // + // Destruction will fail if there are any owned account to prevent loss of access to an account + access(contract) fun burnCallback() { + pre { + // Prevent accidental burning of a resource that has ownership of other accounts + self.ownedAccounts.length == 0: "cannot destroy a manager with owned accounts" + } + + for c in self.childAccounts.keys { + self.removeChild(addr: c) + } } } @@ -501,27 +519,29 @@ pub contract HybridCustody { /// able to manage all ChildAccount resources it shares, without worrying about whether the upstream parent can do /// anything to prevent it. /// - pub resource ChildAccount: AccountPrivate, AccountPublic, MetadataViews.Resolver { + access(all) resource ChildAccount: AccountPrivate, AccountPublic, ViewResolver.Resolver, Burner.Burnable { + access(all) event ResourceDestroyed(uuid: UInt64 = self.uuid, address: Address = self.childCap.address, parent: Address = self.parent) + /// A Capability providing access to the underlying child account - access(self) let childCap: Capability<&{BorrowableAccount, OwnedAccountPublic, MetadataViews.Resolver}> + access(self) let childCap: Capability<&{BorrowableAccount, OwnedAccountPublic, ViewResolver.Resolver}> /// The CapabilityFactory Manager is a ChildAccount's way of limiting what types can be asked for by its parent /// account. The CapabilityFactory returns Capabilities which can be casted to their appropriate types once /// obtained, but only if the child account has configured their factory to allow it. For instance, a /// ChildAccount might choose to expose NonFungibleToken.Provider, but not FungibleToken.Provider - pub var factory: Capability<&CapabilityFactory.Manager{CapabilityFactory.Getter}> + access(self) var factory: Capability<&CapabilityFactory.Manager> /// The CapabilityFilter is a restriction put at the front of obtaining any non-public Capability. Some wallets /// might want to give access to NonFungibleToken.Provider, but only to **some** of the collections it manages, /// not all of them. - pub var filter: Capability<&{CapabilityFilter.Filter}> + access(self) var filter: Capability<&{CapabilityFilter.Filter}> /// The CapabilityDelegator is a way to share one-off capabilities from the child account. These capabilities /// can be public OR private and are separate from the factory which returns a capability at a given path as a /// certain type. When using the CapabilityDelegator, you do not have the ability to specify which path a /// capability came from. For instance, Dapper Wallet might choose to expose a Capability to their Full TopShot /// collection, but only to the path that the collection exists in. - pub let delegator: Capability<&CapabilityDelegator.Delegator{CapabilityDelegator.GetterPublic, CapabilityDelegator.GetterPrivate}> + access(self) let delegator: Capability /// managerCapabilityFilter is a component optionally given to a child account when a manager redeems it. If /// this filter is not nil, any Capability returned through the `getCapability` function checks that the @@ -536,11 +556,11 @@ pub contract HybridCustody { /// ChildAccount resources have a 1:1 association with parent accounts, the named parent Address here is the /// one with a Capability on this resource. - pub let parent: Address + access(all) let parent: Address /// Returns the Address of the underlying child account /// - pub fun getAddress(): Address { + access(all) view fun getAddress(): Address { return self.childCap.address } @@ -560,24 +580,24 @@ pub contract HybridCustody { /// Sets the CapabiltyFactory.Manager Capability /// - pub fun setCapabilityFactory(cap: Capability<&CapabilityFactory.Manager{CapabilityFactory.Getter}>) { + access(contract) fun setCapabilityFactory(cap: Capability<&CapabilityFactory.Manager>) { self.factory = cap } /// Sets the Filter Capability as the one provided /// - pub fun setCapabilityFilter(cap: Capability<&{CapabilityFilter.Filter}>) { + access(contract) fun setCapabilityFilter(cap: Capability<&{CapabilityFilter.Filter}>) { self.filter = cap } - /// The main function to a child account's capabilities from a parent account. When a PrivatePath type is used, - /// the CapabilityFilter will be borrowed and the Capability being returned will be checked against it to - /// ensure that borrowing is permitted. + /// The main function to a child account's capabilities from a parent account. When getting a capability, the CapabilityFilter will be borrowed and + /// the Capability being returned will be checked against it to + /// ensure that borrowing is permitted. If not allowed, nil is returned. /// Also know that this method retrieves Capabilities via the CapabilityFactory path. To retrieve arbitrary /// Capabilities, see `getPrivateCapFromDelegator()` and `getPublicCapFromDelegator()` which use the /// `Delegator` retrieval path. /// - pub fun getCapability(path: CapabilityPath, type: Type): Capability? { + access(Child) view fun getCapability(controllerID: UInt64, type: Type): Capability? { let child = self.childCap.borrow() ?? panic("failed to borrow child account") let f = self.factory.borrow()!.getFactory(type) @@ -585,12 +605,17 @@ pub contract HybridCustody { return nil } - let acct = child.borrowAccount() + let acct = child._borrowAccount() + let tmp = f!.getCapability(acct: acct, controllerID: controllerID) + if tmp == nil { + return nil + } - let cap = f!.getCapability(acct: acct, path: path) - - if path.getType() == Type() { - assert(self.filter.borrow()!.allowed(cap: cap), message: "requested capability is not allowed") + let cap = tmp! + // Check that private capabilities are allowed by either internal or manager filter (if assigned) + // If not allowed, return nil + if self.filter.borrow()!.allowed(cap: cap) == false || (self.getManagerCapabilityFilter()?.allowed(cap: cap) ?? true) == false { + return nil } return cap @@ -599,7 +624,7 @@ pub contract HybridCustody { /// Retrieves a private Capability from the Delegator or nil none is found of the given type. Useful for /// arbitrary Capability retrieval /// - pub fun getPrivateCapFromDelegator(type: Type): Capability? { + access(Child) view fun getPrivateCapFromDelegator(type: Type): Capability? { if let d = self.delegator.borrow() { return d.getPrivateCapability(type) } @@ -610,7 +635,7 @@ pub contract HybridCustody { /// Retrieves a public Capability from the Delegator or nil none is found of the given type. Useful for /// arbitrary Capability retrieval /// - pub fun getPublicCapFromDelegator(type: Type): Capability? { + access(all) view fun getPublicCapFromDelegator(type: Type): Capability? { if let d = self.delegator.borrow() { return d.getPublicCapability(type) } @@ -620,37 +645,43 @@ pub contract HybridCustody { /// Enables retrieval of public Capabilities of the given type from the specified path or nil if none is found. /// Callers should be aware this method uses the `CapabilityFactory` retrieval path. /// - pub fun getPublicCapability(path: PublicPath, type: Type): Capability? { - return self.getCapability(path: path, type: type) + access(all) view fun getPublicCapability(path: PublicPath, type: Type): Capability? { + let child = self.childCap.borrow() ?? panic("failed to borrow child account") + + let f = self.factory.borrow()!.getFactory(type) + if f == nil { + return nil + } + + let acct = child._borrowAccount() + return f!.getPublicCapability(acct: acct, path: path) } /// Returns a reference to the stored managerCapabilityFilter if one exists /// - pub fun getManagerCapabilityFilter(): &{CapabilityFilter.Filter}? { + access(all) view fun getManagerCapabilityFilter(): &{CapabilityFilter.Filter}? { return self.managerCapabilityFilter != nil ? self.managerCapabilityFilter!.borrow() : nil } /// Sets the child account as redeemed by the given Address /// access(contract) fun setRedeemed(_ addr: Address) { - let acct = self.childCap.borrow()!.borrowAccount() - if let o = acct.borrow<&OwnedAccount>(from: HybridCustody.OwnedAccountStoragePath) { - o.setRedeemed(addr) - } + let acct = self.childCap.borrow()!._borrowAccount() + acct.storage.borrow<&OwnedAccount>(from: HybridCustody.OwnedAccountStoragePath)?.setRedeemed(addr) } /// Returns a reference to the stored delegator, generally used for arbitrary Capability retrieval /// - pub fun borrowCapabilityDelegator(): &CapabilityDelegator.Delegator? { + access(Owner) fun borrowCapabilityDelegator(): auth(CapabilityDelegator.Get) &CapabilityDelegator.Delegator? { let path = HybridCustody.getCapabilityDelegatorIdentifier(self.parent) - return self.childCap.borrow()!.borrowAccount().borrow<&CapabilityDelegator.Delegator>( + return self.childCap.borrow()!._borrowAccount().storage.borrow( from: StoragePath(identifier: path)! ) } /// Returns a list of supported metadata views /// - pub fun getViews(): [Type] { + access(all) view fun getViews(): [Type] { return [ Type() ] @@ -658,17 +689,22 @@ pub contract HybridCustody { /// Resolves a view of the given type if supported /// - pub fun resolveView(_ view: Type): AnyStruct? { + access(all) fun resolveView(_ view: Type): AnyStruct? { switch view { case Type(): let childAddress = self.getAddress() - let manager = getAccount(self.parent).getCapability<&HybridCustody.Manager{HybridCustody.ManagerPublic}>(HybridCustody.ManagerPublicPath) + let tmp = getAccount(self.parent).capabilities.get<&{HybridCustody.ManagerPublic}>(HybridCustody.ManagerPublicPath) + if tmp == nil { + return nil + } + + let manager = tmp! if !manager.check() { return nil } - return manager!.borrow()!.getChildAccountDisplay(address: childAddress) + return manager.borrow()!.getChildAccountDisplay(address: childAddress) } return nil } @@ -681,22 +717,22 @@ pub contract HybridCustody { return } - let child: &AnyResource{HybridCustody.BorrowableAccount} = self.childCap.borrow()! + let child: &{HybridCustody.BorrowableAccount} = self.childCap.borrow()! if !child.check() { return } - let acct = child.borrowAccount() - if let ownedAcct = acct.borrow<&OwnedAccount>(from: HybridCustody.OwnedAccountStoragePath) { + let acct = child._borrowAccount() + if let ownedAcct = acct.storage.borrow(from: HybridCustody.OwnedAccountStoragePath) { ownedAcct.removeParent(parent: parent) } } init( - _ childCap: Capability<&{BorrowableAccount, OwnedAccountPublic, MetadataViews.Resolver}>, - _ factory: Capability<&CapabilityFactory.Manager{CapabilityFactory.Getter}>, + _ childCap: Capability<&{BorrowableAccount, OwnedAccountPublic, ViewResolver.Resolver}>, + _ factory: Capability<&CapabilityFactory.Manager>, _ filter: Capability<&{CapabilityFilter.Filter}>, - _ delegator: Capability<&CapabilityDelegator.Delegator{CapabilityDelegator.GetterPublic, CapabilityDelegator.GetterPrivate}>, + _ delegator: Capability, _ parent: Address ) { pre { @@ -716,8 +752,30 @@ pub contract HybridCustody { self.resources <- {} } - destroy () { - destroy <- self.resources + /// Returns a capability to this child account's CapabilityFilter + /// + access(all) view fun getCapabilityFilter(): &{CapabilityFilter.Filter}? { + return self.filter.check() ? self.filter.borrow() : nil + } + + /// Returns a capability to this child account's CapabilityFactory + /// + access(all) view fun getCapabilityFactoryManager(): &{CapabilityFactory.Getter}? { + return self.factory.check() ? self.factory.borrow() : nil + } + + access(all) view fun getControllerIDForType(type: Type, forPath: StoragePath): UInt64? { + let child = self.childCap.borrow() + if child == nil { + return nil + } + + return child!.getControllerIDForType(type: type, forPath: forPath) + } + + // When a ChildAccount is destroyed, attempt to remove it from the parent account as well + access(contract) fun burnCallback() { + self.parentRemoveChildCallback(parent: self.parent) } } @@ -730,18 +788,19 @@ pub contract HybridCustody { /// accounts would still exist, allowing a form of Hybrid Custody which has no true owner over an account, but /// shared partial ownership. /// - pub resource OwnedAccount: OwnedAccountPrivate, BorrowableAccount, OwnedAccountPublic, MetadataViews.Resolver { + access(all) resource OwnedAccount: OwnedAccountPrivate, BorrowableAccount, OwnedAccountPublic, ViewResolver.Resolver, Burner.Burnable { + access(all) event ResourceDestroyed(uuid: UInt64 = self.uuid, addr: Address = self.acct.address) /// Capability on the underlying account object - access(self) var acct: Capability<&AuthAccount> + access(self) var acct: Capability /// Mapping of current and pending parents, true and false respectively - pub let parents: {Address: Bool} + access(all) let parents: {Address: Bool} /// Address of the pending owner, if one exists - pub var pendingOwner: Address? + access(all) var pendingOwner: Address? /// Address of the current owner, if one exists - pub var acctOwner: Address? + access(all) var acctOwner: Address? /// Owned status of this account - pub var currentlyOwned: Bool + access(all) var currentlyOwned: Bool /// A bucket of structs so that the OwnedAccount resource can be easily extended with new functionality. access(self) let data: {String: AnyStruct} @@ -792,12 +851,12 @@ pub contract HybridCustody { /// 4. Publish the newly made private link to the designated parent's inbox for them to claim on their @Manager /// resource. /// - pub fun publishToParent( + access(Owner) fun publishToParent( parentAddress: Address, - factory: Capability<&CapabilityFactory.Manager{CapabilityFactory.Getter}>, + factory: Capability<&CapabilityFactory.Manager>, filter: Capability<&{CapabilityFilter.Filter}> ) { - pre{ + pre { self.parents[parentAddress] == nil: "Address pending or already redeemed as parent" } let capDelegatorIdentifier = HybridCustody.getCapabilityDelegatorIdentifier(parentAddress) @@ -808,41 +867,30 @@ pub contract HybridCustody { let capDelegatorStorage = StoragePath(identifier: capDelegatorIdentifier)! let acct = self.borrowAccount() - assert(acct.borrow<&AnyResource>(from: capDelegatorStorage) == nil, message: "conflicting resource found in capability delegator storage slot for parentAddress") - assert(acct.borrow<&AnyResource>(from: childAccountStorage) == nil, message: "conflicting resource found in child account storage slot for parentAddress") + assert(acct.storage.borrow<&AnyResource>(from: capDelegatorStorage) == nil, message: "conflicting resource found in capability delegator storage slot for parentAddress") + assert(acct.storage.borrow<&AnyResource>(from: childAccountStorage) == nil, message: "conflicting resource found in child account storage slot for parentAddress") - if acct.borrow<&CapabilityDelegator.Delegator>(from: capDelegatorStorage) == nil { + if acct.storage.borrow<&CapabilityDelegator.Delegator>(from: capDelegatorStorage) == nil { let delegator <- CapabilityDelegator.createDelegator() - acct.save(<-delegator, to: capDelegatorStorage) + acct.storage.save(<-delegator, to: capDelegatorStorage) } let capDelegatorPublic = PublicPath(identifier: capDelegatorIdentifier)! - let capDelegatorPrivate = PrivatePath(identifier: capDelegatorIdentifier)! - acct.link<&CapabilityDelegator.Delegator{CapabilityDelegator.GetterPublic}>( - capDelegatorPublic, - target: capDelegatorStorage - ) - acct.link<&CapabilityDelegator.Delegator{CapabilityDelegator.GetterPublic, CapabilityDelegator.GetterPrivate}>( - capDelegatorPrivate, - target: capDelegatorStorage - ) - let delegator = acct.getCapability<&CapabilityDelegator.Delegator{CapabilityDelegator.GetterPublic, CapabilityDelegator.GetterPrivate}>( - capDelegatorPrivate - ) + let pubCap = acct.capabilities.storage.issue<&{CapabilityDelegator.GetterPublic}>(capDelegatorStorage) + acct.capabilities.publish(pubCap, at: capDelegatorPublic) + + let delegator = acct.capabilities.storage.issue(capDelegatorStorage) assert(delegator.check(), message: "failed to setup capability delegator for parent address") - let borrowableCap = self.borrowAccount().getCapability<&{BorrowableAccount, OwnedAccountPublic, MetadataViews.Resolver}>( - HybridCustody.OwnedAccountPrivatePath + let borrowableCap = self.borrowAccount().capabilities.storage.issue<&{BorrowableAccount, OwnedAccountPublic, ViewResolver.Resolver}>( + HybridCustody.OwnedAccountStoragePath ) - let childAcct <- create ChildAccount(borrowableCap, factory, filter, delegator, parentAddress) - let childAccountPrivatePath = PrivatePath(identifier: identifier)! + let childAcct <- create ChildAccount(borrowableCap, factory, filter, delegator, parentAddress) - acct.save(<-childAcct, to: childAccountStorage) - acct.link<&ChildAccount{AccountPrivate, AccountPublic, MetadataViews.Resolver}>(childAccountPrivatePath, target: childAccountStorage) - - let delegatorCap = acct.getCapability<&ChildAccount{AccountPrivate, AccountPublic, MetadataViews.Resolver}>(childAccountPrivatePath) + acct.storage.save(<-childAcct, to: childAccountStorage) + let delegatorCap = acct.capabilities.storage.issue(childAccountStorage) assert(delegatorCap.check(), message: "Delegator capability check failed") acct.inbox.publish(delegatorCap, name: identifier, recipient: parentAddress) @@ -862,38 +910,43 @@ pub contract HybridCustody { /// Checks the validity of the encapsulated account Capability /// - pub fun check(): Bool { + access(all) view fun check(): Bool { return self.acct.check() } /// Returns a reference to the encapsulated account object /// - pub fun borrowAccount(): &AuthAccount { - return self.acct.borrow()! + access(Owner) view fun borrowAccount(): auth(Storage, Contracts, Keys, Inbox, Capabilities) &Account { + return self.acct.borrow() ?? panic("unable to borrow Account Capability") + } + + // Used internally so that child account resources are able to borrow their underlying Account reference + access(contract) view fun _borrowAccount(): auth(Storage, Contracts, Keys, Inbox, Capabilities) &Account { + return self.borrowAccount() } /// Returns the addresses of all associated parents pending and active /// - pub fun getParentAddresses(): [Address] { + access(all) view fun getParentAddresses(): [Address] { return self.parents.keys } /// Returns whether the given address is a parent of this account /// - pub fun isChildOf(_ addr: Address): Bool { + access(all) view fun isChildOf(_ addr: Address): Bool { return self.parents[addr] != nil } /// Returns nil if the given address is not a parent, false if the parent has not redeemed the child account /// yet, and true if they have /// - pub fun getRedeemedStatus(addr: Address): Bool? { + access(all) view fun getRedeemedStatus(addr: Address): Bool? { return self.parents[addr] } /// Returns associated parent addresses and their redeemed status /// - pub fun getParentStatuses(): {Address: Bool} { + access(all) view fun getParentStatuses(): {Address: Bool} { return self.parents } @@ -901,29 +954,37 @@ pub contract HybridCustody { /// configured for the provided parent address. Once done, the parent will not have any valid capabilities with /// which to access the child account. /// - pub fun removeParent(parent: Address): Bool { + access(Owner) fun removeParent(parent: Address): Bool { if self.parents[parent] == nil { return false } + let identifier = HybridCustody.getChildAccountIdentifier(parent) let capDelegatorIdentifier = HybridCustody.getCapabilityDelegatorIdentifier(parent) let acct = self.borrowAccount() - acct.unlink(PrivatePath(identifier: identifier)!) - acct.unlink(PublicPath(identifier: identifier)!) - acct.unlink(PrivatePath(identifier: capDelegatorIdentifier)!) - acct.unlink(PublicPath(identifier: capDelegatorIdentifier)!) + // get all controllers which target this storage path + let storagePath = StoragePath(identifier: identifier)! + let childAccountControllers = acct.capabilities.storage.getControllers(forPath: storagePath) + for c in childAccountControllers { + c.delete() + } + Burner.burn(<- acct.storage.load<@AnyResource>(from: storagePath)) - destroy <- acct.load<@AnyResource>(from: StoragePath(identifier: identifier)!) - destroy <- acct.load<@AnyResource>(from: StoragePath(identifier: capDelegatorIdentifier)!) + let delegatorStoragePath = StoragePath(identifier: capDelegatorIdentifier)! + let delegatorControllers = acct.capabilities.storage.getControllers(forPath: delegatorStoragePath) + for c in delegatorControllers { + c.delete() + } + Burner.burn(<- acct.storage.load<@AnyResource>(from: delegatorStoragePath)) self.parents.remove(key: parent) emit AccountUpdated(id: self.uuid, child: self.acct.address, parent: parent, active: false) - let parentManager = getAccount(parent).getCapability<&Manager{ManagerPublic}>(HybridCustody.ManagerPublicPath) + let parentManager = getAccount(parent).capabilities.get<&{ManagerPublic}>(HybridCustody.ManagerPublicPath) if parentManager.check() { - parentManager.borrow()?.removeParentCallback(child: self.owner!.address) + parentManager.borrow()?.removeParentCallback(child: acct.address) } return true @@ -931,21 +992,21 @@ pub contract HybridCustody { /// Returns the address of the encapsulated account /// - pub fun getAddress(): Address { + access(all) view fun getAddress(): Address { return self.acct.address } /// Returns the address of the pending owner if one is assigned. Pending owners are assigned when ownership has /// been granted, but has not yet been redeemed. /// - pub fun getPendingOwner(): Address? { + access(all) view fun getPendingOwner(): Address? { return self.pendingOwner } /// Returns the address of the current owner if one is assigned. Current owners are assigned when ownership has /// been redeemed. /// - pub fun getOwner(): Address? { + access(all) view fun getOwner(): Address? { if !self.currentlyOwned { return nil } @@ -961,22 +1022,17 @@ pub contract HybridCustody { /// mechanism intended to easily transfer 'root' access on this account to another account and an attempt to /// minimize access vectors. /// - pub fun giveOwnership(to: Address) { + access(Owner) fun giveOwnership(to: Address) { self.seal() let acct = self.borrowAccount() - // Unlink existing owner's Capability if owner exists - if self.acctOwner != nil { - acct.unlink( - PrivatePath(identifier: HybridCustody.getOwnerIdentifier(self.acctOwner!))! - ) - } + // Link a Capability for the new owner, retrieve & publish let identifier = HybridCustody.getOwnerIdentifier(to) - let cap = acct.link<&{OwnedAccountPrivate, OwnedAccountPublic, MetadataViews.Resolver}>( - PrivatePath(identifier: identifier)!, - target: HybridCustody.OwnedAccountStoragePath - ) ?? panic("failed to link child account capability") + let cap = acct.capabilities.storage.issue(HybridCustody.OwnedAccountStoragePath) + + // make sure we can borrow the newly issued owned account + cap.borrow()?.borrowAccount() ?? panic("can not borrow the Hybrid Custody Owned Account") acct.inbox.publish(cap, name: identifier, recipient: to) @@ -988,7 +1044,7 @@ pub contract HybridCustody { /// Revokes all keys on the underlying account /// - pub fun revokeAllKeys() { + access(Owner) fun revokeAllKeys() { let acct = self.borrowAccount() // Revoke all keys @@ -1007,33 +1063,25 @@ pub contract HybridCustody { /// assumes ownership of an account to guarantee that the previous owner doesn't maintain admin access to the /// account via other AuthAccount Capabilities. /// - pub fun rotateAuthAccount() { + access(Owner) fun rotateAuthAccount() { let acct = self.borrowAccount() // Find all active AuthAccount capabilities so they can be removed after we make the new auth account cap - let pathsToUnlink: [PrivatePath] = [] - acct.forEachPrivate(fun (path: PrivatePath, type: Type): Bool { - if type.identifier == "Capability<&AuthAccount>" { - pathsToUnlink.append(path) - } - return true - }) + let controllersToDestroy = acct.capabilities.account.getControllers() // Link a new AuthAccount Capability - // NOTE: This path cannot be sufficiently randomly generated, an app calling this function could build a - // capability to this path before it is made, thus maintaining ownership despite making it look like they - // gave it away. Until capability controllers, this method should not be fully trusted. - let authAcctPath = "HybridCustodyRelinquished".concat(HybridCustody.account.address.toString()).concat(getCurrentBlock().height.toString()) - let acctCap = acct.linkAccount(PrivatePath(identifier: authAcctPath)!)! + let acctCap = acct.capabilities.account.issue() self.acct = acctCap let newAcct = self.acct.borrow()! // cleanup, remove all previously found paths. We had to do it in this order because we will be unlinking // the existing path which will cause a deference issue with the originally borrowed auth account - for p in pathsToUnlink { - newAcct.unlink(p) + for con in controllersToDestroy { + newAcct.capabilities.account.getController(byCapabilityID: con.capabilityID)?.delete() } + + assert(self.acct.check(), message: "new auth account capability is not valid") } /// Revokes all keys on an account, unlinks all currently active AuthAccount capabilities, then makes a new one @@ -1043,7 +1091,7 @@ pub contract HybridCustody { /// /// USE WITH EXTREME CAUTION. /// - pub fun seal() { + access(Owner) fun seal() { self.rotateAuthAccount() self.revokeAllKeys() // There needs to be a path to giving ownership that doesn't revoke keys emit AccountSealed(id: self.uuid, address: self.acct.address, parents: self.parents.keys) @@ -1052,16 +1100,16 @@ pub contract HybridCustody { /// Retrieves a reference to the ChildAccount associated with the given parent account if one exists. /// - pub fun borrowChildAccount(parent: Address): &ChildAccount? { + access(Owner) fun borrowChildAccount(parent: Address): auth(Child) &ChildAccount? { let identifier = HybridCustody.getChildAccountIdentifier(parent) - return self.borrowAccount().borrow<&ChildAccount>(from: StoragePath(identifier: identifier)!) + return self.borrowAccount().storage.borrow(from: StoragePath(identifier: identifier)!) } /// Sets the CapabilityFactory Manager for the specified parent in the associated ChildAccount. /// - pub fun setCapabilityFactoryForParent( + access(Owner) fun setCapabilityFactoryForParent( parent: Address, - cap: Capability<&CapabilityFactory.Manager{CapabilityFactory.Getter}> + cap: Capability<&CapabilityFactory.Manager> ) { let p = self.borrowChildAccount(parent: parent) ?? panic("could not find parent address") p.setCapabilityFactory(cap: cap) @@ -1069,21 +1117,21 @@ pub contract HybridCustody { /// Sets the Filter for the specified parent in the associated ChildAccount. /// - pub fun setCapabilityFilterForParent(parent: Address, cap: Capability<&{CapabilityFilter.Filter}>) { + access(Owner) fun setCapabilityFilterForParent(parent: Address, cap: Capability<&{CapabilityFilter.Filter}>) { let p = self.borrowChildAccount(parent: parent) ?? panic("could not find parent address") p.setCapabilityFilter(cap: cap) } /// Retrieves a reference to the Delegator associated with the given parent account if one exists. /// - pub fun borrowCapabilityDelegatorForParent(parent: Address): &CapabilityDelegator.Delegator? { + access(Owner) fun borrowCapabilityDelegatorForParent(parent: Address): auth(CapabilityDelegator.Get, CapabilityDelegator.Add, CapabilityDelegator.Delete) &CapabilityDelegator.Delegator? { let identifier = HybridCustody.getCapabilityDelegatorIdentifier(parent) - return self.borrowAccount().borrow<&CapabilityDelegator.Delegator>(from: StoragePath(identifier: identifier)!) + return self.borrowAccount().storage.borrow(from: StoragePath(identifier: identifier)!) } /// Adds the provided Capability to the Delegator associated with the given parent account. /// - pub fun addCapabilityToDelegator(parent: Address, cap: Capability, isPublic: Bool) { + access(Owner) fun addCapabilityToDelegator(parent: Address, cap: Capability, isPublic: Bool) { let p = self.borrowChildAccount(parent: parent) ?? panic("could not find parent address") let delegator = self.borrowCapabilityDelegatorForParent(parent: parent) ?? panic("could not borrow capability delegator resource for parent address") @@ -1092,20 +1140,20 @@ pub contract HybridCustody { /// Removes the provided Capability from the Delegator associated with the given parent account. /// - pub fun removeCapabilityFromDelegator(parent: Address, cap: Capability) { + access(Owner) fun removeCapabilityFromDelegator(parent: Address, cap: Capability) { let p = self.borrowChildAccount(parent: parent) ?? panic("could not find parent address") let delegator = self.borrowCapabilityDelegatorForParent(parent: parent) ?? panic("could not borrow capability delegator resource for parent address") delegator.removeCapability(cap: cap) } - pub fun getViews(): [Type] { + access(all) view fun getViews(): [Type] { return [ Type() ] } - pub fun resolveView(_ view: Type): AnyStruct? { + access(all) fun resolveView(_ view: Type): AnyStruct? { switch view { case Type(): return self.display @@ -1115,12 +1163,27 @@ pub contract HybridCustody { /// Sets this OwnedAccount's display to the one provided /// - pub fun setDisplay(_ d: MetadataViews.Display) { + access(Owner) fun setDisplay(_ d: MetadataViews.Display) { self.display = d } + access(all) view fun getControllerIDForType(type: Type, forPath: StoragePath): UInt64? { + let acct = self.acct.borrow() + if acct == nil { + return nil + } + + for c in acct!.capabilities.storage.getControllers(forPath: forPath) { + if c.borrowType.isSubtype(of: type) { + return c.capabilityID + } + } + + return nil + } + init( - _ acct: Capability<&AuthAccount> + _ acct: Capability ) { self.acct = acct @@ -1134,36 +1197,39 @@ pub contract HybridCustody { self.display = nil } - destroy () { - destroy <- self.resources + // When an OwnedAccount is destroyed, remove it from every configured parent account + access(contract) fun burnCallback() { + for p in self.parents.keys { + self.removeParent(parent: p) + } } } /// Utility function to get the path identifier for a parent address when interacting with a ChildAccount and its /// parents /// - pub fun getChildAccountIdentifier(_ addr: Address): String { + access(all) view fun getChildAccountIdentifier(_ addr: Address): String { return "ChildAccount_".concat(addr.toString()) } /// Utility function to get the path identifier for a parent address when interacting with a Delegator and its /// parents /// - pub fun getCapabilityDelegatorIdentifier(_ addr: Address): String { + access(all) view fun getCapabilityDelegatorIdentifier(_ addr: Address): String { return "ChildCapabilityDelegator_".concat(addr.toString()) } /// Utility function to get the path identifier for a parent address when interacting with an OwnedAccount and its /// owners /// - pub fun getOwnerIdentifier(_ addr: Address): String { + access(all) view fun getOwnerIdentifier(_ addr: Address): String { return "HybridCustodyOwnedAccount_".concat(HybridCustody.account.address.toString()).concat(addr.toString()) } /// Returns an OwnedAccount wrapping the provided AuthAccount Capability. /// - pub fun createOwnedAccount( - acct: Capability<&AuthAccount> + access(all) fun createOwnedAccount( + acct: Capability ): @OwnedAccount { pre { acct.check(): "invalid auth account capability" @@ -1176,7 +1242,7 @@ pub contract HybridCustody { /// Returns a new Manager with the provided Filter as default (if not nil). /// - pub fun createManager(filter: Capability<&{CapabilityFilter.Filter}>?): @Manager { + access(all) fun createManager(filter: Capability<&{CapabilityFilter.Filter}>?): @Manager { pre { filter == nil || filter!.check(): "Invalid CapabilityFilter Filter capability provided" } @@ -1188,15 +1254,10 @@ pub contract HybridCustody { init() { let identifier = "HybridCustodyChild_".concat(self.account.address.toString()) self.OwnedAccountStoragePath = StoragePath(identifier: identifier)! - self.OwnedAccountPrivatePath = PrivatePath(identifier: identifier)! self.OwnedAccountPublicPath = PublicPath(identifier: identifier)! - self.LinkedAccountPrivatePath = PrivatePath(identifier: "LinkedAccountPrivatePath_".concat(identifier))! - self.BorrowableAccountPrivatePath = PrivatePath(identifier: "BorrowableAccountPrivatePath_".concat(identifier))! - let managerIdentifier = "HybridCustodyManager_".concat(self.account.address.toString()) self.ManagerStoragePath = StoragePath(identifier: managerIdentifier)! self.ManagerPublicPath = PublicPath(identifier: managerIdentifier)! - self.ManagerPrivatePath = PrivatePath(identifier: managerIdentifier)! } -} \ No newline at end of file +} diff --git a/contracts/hybrid-custody/factories/FTAllFactory.cdc b/contracts/hybrid-custody/factories/FTAllFactory.cdc index 3b8facd..046991e 100644 --- a/contracts/hybrid-custody/factories/FTAllFactory.cdc +++ b/contracts/hybrid-custody/factories/FTAllFactory.cdc @@ -1,10 +1,22 @@ import "CapabilityFactory" import "FungibleToken" -pub contract FTAllFactory { - pub struct Factory: CapabilityFactory.Factory { - pub fun getCapability(acct: &AuthAccount, path: CapabilityPath): Capability { - return acct.getCapability<&{FungibleToken.Provider, FungibleToken.Balance, FungibleToken.Receiver}>(path) +access(all) contract FTAllFactory { + access(all) struct Factory: CapabilityFactory.Factory { + access(all) view fun getCapability(acct: auth(Capabilities) &Account, controllerID: UInt64): Capability? { + if let con = acct.capabilities.storage.getController(byCapabilityID: controllerID) { + if !con.capability.check() { + return nil + } + + return con.capability as! Capability + } + + return nil + } + + access(all) view fun getPublicCapability(acct: &Account, path: PublicPath): Capability? { + return nil } } } \ No newline at end of file diff --git a/contracts/hybrid-custody/factories/FTBalanceFactory.cdc b/contracts/hybrid-custody/factories/FTBalanceFactory.cdc index bd9d097..f4b70c1 100644 --- a/contracts/hybrid-custody/factories/FTBalanceFactory.cdc +++ b/contracts/hybrid-custody/factories/FTBalanceFactory.cdc @@ -1,10 +1,22 @@ import "CapabilityFactory" import "FungibleToken" -pub contract FTBalanceFactory { - pub struct Factory: CapabilityFactory.Factory { - pub fun getCapability(acct: &AuthAccount, path: CapabilityPath): Capability { - return acct.getCapability<&{FungibleToken.Balance}>(path) +access(all) contract FTBalanceFactory { + access(all) struct Factory: CapabilityFactory.Factory { + access(all) view fun getCapability(acct: auth(Capabilities) &Account, controllerID: UInt64): Capability? { + if let con = acct.capabilities.storage.getController(byCapabilityID: controllerID) { + if !con.capability.check<&{FungibleToken.Balance}>() { + return nil + } + + return con.capability as! Capability<&{FungibleToken.Balance}> + } + + return nil + } + + access(all) view fun getPublicCapability(acct: &Account, path: PublicPath): Capability? { + return acct.capabilities.get<&{FungibleToken.Balance}>(path) } } } \ No newline at end of file diff --git a/contracts/hybrid-custody/factories/FTProviderFactory.cdc b/contracts/hybrid-custody/factories/FTProviderFactory.cdc index 6f36701..bff3175 100644 --- a/contracts/hybrid-custody/factories/FTProviderFactory.cdc +++ b/contracts/hybrid-custody/factories/FTProviderFactory.cdc @@ -1,10 +1,22 @@ import "CapabilityFactory" import "FungibleToken" -pub contract FTProviderFactory { - pub struct Factory: CapabilityFactory.Factory { - pub fun getCapability(acct: &AuthAccount, path: CapabilityPath): Capability { - return acct.getCapability<&{FungibleToken.Provider}>(path) +access(all) contract FTProviderFactory { + access(all) struct Factory: CapabilityFactory.Factory { + access(all) view fun getCapability(acct: auth(Capabilities) &Account, controllerID: UInt64): Capability? { + if let con = acct.capabilities.storage.getController(byCapabilityID: controllerID) { + if !con.capability.check() { + return nil + } + + return con.capability as! Capability + } + + return nil + } + + access(all) view fun getPublicCapability(acct: &Account, path: PublicPath): Capability? { + return nil } } -} +} \ No newline at end of file diff --git a/contracts/hybrid-custody/factories/FTReceiverBalanceFactory.cdc b/contracts/hybrid-custody/factories/FTReceiverBalanceFactory.cdc index 49673d5..dd87c1c 100644 --- a/contracts/hybrid-custody/factories/FTReceiverBalanceFactory.cdc +++ b/contracts/hybrid-custody/factories/FTReceiverBalanceFactory.cdc @@ -1,10 +1,22 @@ import "CapabilityFactory" import "FungibleToken" -pub contract FTReceiverBalanceFactory { - pub struct Factory: CapabilityFactory.Factory { - pub fun getCapability(acct: &AuthAccount, path: CapabilityPath): Capability { - return acct.getCapability<&{FungibleToken.Receiver, FungibleToken.Balance}>(path) +access(all) contract FTReceiverBalanceFactory { + access(all) struct Factory: CapabilityFactory.Factory { + access(all) view fun getCapability(acct: auth(Capabilities) &Account, controllerID: UInt64): Capability? { + if let con = acct.capabilities.storage.getController(byCapabilityID: controllerID) { + if !con.capability.check<&{FungibleToken.Receiver, FungibleToken.Balance}>() { + return nil + } + + return con.capability as! Capability<&{FungibleToken.Receiver, FungibleToken.Balance}> + } + + return nil + } + + access(all) view fun getPublicCapability(acct: &Account, path: PublicPath): Capability? { + return acct.capabilities.get<&{FungibleToken.Receiver, FungibleToken.Balance}>(path) } } } \ No newline at end of file diff --git a/contracts/hybrid-custody/factories/FTReceiverFactory.cdc b/contracts/hybrid-custody/factories/FTReceiverFactory.cdc index 03ade5c..815a244 100644 --- a/contracts/hybrid-custody/factories/FTReceiverFactory.cdc +++ b/contracts/hybrid-custody/factories/FTReceiverFactory.cdc @@ -1,10 +1,22 @@ import "CapabilityFactory" import "FungibleToken" -pub contract FTReceiverFactory { - pub struct Factory: CapabilityFactory.Factory { - pub fun getCapability(acct: &AuthAccount, path: CapabilityPath): Capability { - return acct.getCapability<&{FungibleToken.Receiver}>(path) +access(all) contract FTReceiverFactory { + access(all) struct Factory: CapabilityFactory.Factory { + access(all) view fun getCapability(acct: auth(Capabilities) &Account, controllerID: UInt64): Capability? { + if let con = acct.capabilities.storage.getController(byCapabilityID: controllerID) { + if !con.capability.check<&{FungibleToken.Receiver}>() { + return nil + } + + return con.capability as! Capability<&{FungibleToken.Receiver}> + } + + return nil + } + + access(all) view fun getPublicCapability(acct: &Account, path: PublicPath): Capability? { + return acct.capabilities.get<&{FungibleToken.Receiver}>(path) } } } \ No newline at end of file diff --git a/contracts/hybrid-custody/factories/FTVaultFactory.cdc b/contracts/hybrid-custody/factories/FTVaultFactory.cdc new file mode 100644 index 0000000..c279af4 --- /dev/null +++ b/contracts/hybrid-custody/factories/FTVaultFactory.cdc @@ -0,0 +1,46 @@ +import "CapabilityFactory" +import "FungibleToken" + +access(all) contract FTVaultFactory { + access(all) struct WithdrawFactory: CapabilityFactory.Factory { + access(all) view fun getCapability(acct: auth(Capabilities) &Account, controllerID: UInt64): Capability? { + if let con = acct.capabilities.storage.getController(byCapabilityID: controllerID) { + if !con.capability.check() { + return nil + } + + return con.capability as! Capability + } + + return nil + } + + access(all) view fun getPublicCapability(acct: &Account, path: PublicPath): Capability? { + return nil + } + } + + access(all) struct Factory: CapabilityFactory.Factory { + access(all) view fun getCapability(acct: auth(Capabilities) &Account, controllerID: UInt64): Capability? { + if let con = acct.capabilities.storage.getController(byCapabilityID: controllerID) { + if !con.capability.check<&{FungibleToken.Vault}>() { + return nil + } + + return con.capability as! Capability<&{FungibleToken.Vault}> + } + + return nil + } + + access(all) view fun getPublicCapability(acct: &Account, path: PublicPath): Capability? { + let cap = acct.capabilities.get<&{FungibleToken.Vault}>(path) + if !cap.check() { + return nil + } + + return cap + + } + } +} \ No newline at end of file diff --git a/contracts/hybrid-custody/factories/NFTCollectionFactory.cdc b/contracts/hybrid-custody/factories/NFTCollectionFactory.cdc new file mode 100644 index 0000000..caf4dec --- /dev/null +++ b/contracts/hybrid-custody/factories/NFTCollectionFactory.cdc @@ -0,0 +1,45 @@ +import "CapabilityFactory" +import "NonFungibleToken" + +access(all) contract NFTProviderAndCollectionFactory { + access(all) struct WithdrawFactory: CapabilityFactory.Factory { + access(all) view fun getCapability(acct: auth(Capabilities) &Account, controllerID: UInt64): Capability? { + if let con = acct.capabilities.storage.getController(byCapabilityID: controllerID) { + if !con.capability.check() { + return nil + } + + return con.capability as! Capability + } + + return nil + } + + access(all) view fun getPublicCapability(acct: &Account, path: PublicPath): Capability? { + return nil + } + } + + access(all) struct Factory: CapabilityFactory.Factory { + access(all) view fun getCapability(acct: auth(Capabilities) &Account, controllerID: UInt64): Capability? { + if let con = acct.capabilities.storage.getController(byCapabilityID: controllerID) { + if !con.capability.check<&{NonFungibleToken.Collection}>() { + return nil + } + + return con.capability as! Capability<&{NonFungibleToken.Collection}> + } + + return nil + } + + access(all) view fun getPublicCapability(acct: &Account, path: PublicPath): Capability? { + let cap = acct.capabilities.get<&{NonFungibleToken.Collection}>(path) + if !cap.check() { + return nil + } + + return cap + } + } +} \ No newline at end of file diff --git a/contracts/hybrid-custody/factories/NFTCollectionPublicFactory.cdc b/contracts/hybrid-custody/factories/NFTCollectionPublicFactory.cdc index 0e9df83..16edaad 100644 --- a/contracts/hybrid-custody/factories/NFTCollectionPublicFactory.cdc +++ b/contracts/hybrid-custody/factories/NFTCollectionPublicFactory.cdc @@ -1,10 +1,22 @@ import "CapabilityFactory" import "NonFungibleToken" -pub contract NFTCollectionPublicFactory { - pub struct Factory: CapabilityFactory.Factory { - pub fun getCapability(acct: &AuthAccount, path: CapabilityPath): Capability { - return acct.getCapability<&{NonFungibleToken.CollectionPublic}>(path) +access(all) contract NFTCollectionPublicFactory { + access(all) struct Factory: CapabilityFactory.Factory { + access(all) view fun getCapability(acct: auth(Capabilities) &Account, controllerID: UInt64): Capability? { + if let con = acct.capabilities.storage.getController(byCapabilityID: controllerID) { + if !con.capability.check<&{NonFungibleToken.CollectionPublic}>() { + return nil + } + + return con.capability as! Capability<&{NonFungibleToken.CollectionPublic}> + } + + return nil + } + + access(all) view fun getPublicCapability(acct: &Account, path: PublicPath): Capability? { + return acct.capabilities.get<&{NonFungibleToken.CollectionPublic}>(path) } } } \ No newline at end of file diff --git a/contracts/hybrid-custody/factories/NFTProviderAndCollectionFactory.cdc b/contracts/hybrid-custody/factories/NFTProviderAndCollectionFactory.cdc new file mode 100644 index 0000000..ebeb4e4 --- /dev/null +++ b/contracts/hybrid-custody/factories/NFTProviderAndCollectionFactory.cdc @@ -0,0 +1,22 @@ +import "CapabilityFactory" +import "NonFungibleToken" + +access(all) contract NFTProviderAndCollectionFactory { + access(all) struct Factory: CapabilityFactory.Factory { + access(all) view fun getCapability(acct: auth(Capabilities) &Account, controllerID: UInt64): Capability? { + if let con = acct.capabilities.storage.getController(byCapabilityID: controllerID) { + if !con.capability.check() { + return nil + } + + return con.capability as! Capability + } + + return nil + } + + access(all) view fun getPublicCapability(acct: &Account, path: PublicPath): Capability? { + return nil + } + } +} \ No newline at end of file diff --git a/contracts/hybrid-custody/factories/NFTProviderAndCollectionPublicFactory.cdc b/contracts/hybrid-custody/factories/NFTProviderAndCollectionPublicFactory.cdc deleted file mode 100644 index a79755e..0000000 --- a/contracts/hybrid-custody/factories/NFTProviderAndCollectionPublicFactory.cdc +++ /dev/null @@ -1,10 +0,0 @@ -import "CapabilityFactory" -import "NonFungibleToken" - -pub contract NFTProviderAndCollectionFactory { - pub struct Factory: CapabilityFactory.Factory { - pub fun getCapability(acct: &AuthAccount, path: CapabilityPath): Capability { - return acct.getCapability<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>(path) - } - } -} \ No newline at end of file diff --git a/contracts/hybrid-custody/factories/NFTProviderFactory.cdc b/contracts/hybrid-custody/factories/NFTProviderFactory.cdc index 7bf547e..a2214dc 100644 --- a/contracts/hybrid-custody/factories/NFTProviderFactory.cdc +++ b/contracts/hybrid-custody/factories/NFTProviderFactory.cdc @@ -1,10 +1,22 @@ import "CapabilityFactory" import "NonFungibleToken" -pub contract NFTProviderFactory { - pub struct Factory: CapabilityFactory.Factory { - pub fun getCapability(acct: &AuthAccount, path: CapabilityPath): Capability { - return acct.getCapability<&{NonFungibleToken.Provider}>(path) +access(all) contract NFTProviderFactory { + access(all) struct Factory: CapabilityFactory.Factory { + access(all) view fun getCapability(acct: auth(Capabilities) &Account, controllerID: UInt64): Capability? { + if let con = acct.capabilities.storage.getController(byCapabilityID: controllerID) { + if !con.capability.check() { + return nil + } + + return con.capability as! Capability + } + + return nil + } + + access(all) view fun getPublicCapability(acct: &Account, path: PublicPath): Capability? { + return nil } } } \ No newline at end of file diff --git a/contracts/lost-and-found/FeeEstimator.cdc b/contracts/lost-and-found/FeeEstimator.cdc index 2337b75..23125dc 100644 --- a/contracts/lost-and-found/FeeEstimator.cdc +++ b/contracts/lost-and-found/FeeEstimator.cdc @@ -11,49 +11,41 @@ import "FlowToken" Consumers of this contract would then need to pop the resource out of the DepositEstimate resource to get it back */ -pub contract FeeEstimator { - pub resource DepositEstimate { - pub var item: @AnyResource? - pub var storageFee: UFix64 +access(all) contract FeeEstimator { + access(all) resource DepositEstimate { + access(all) var item: @AnyResource? + access(all) var storageFee: UFix64 init(item: @AnyResource, storageFee: UFix64) { self.item <- item self.storageFee = storageFee } - pub fun withdraw(): @AnyResource { - let resource <- self.item <- nil - return <-resource! - } - - destroy() { - pre { - self.item == nil: "cannot destroy with non-null item" - } - - destroy self.item + access(all) fun withdraw(): @AnyResource { + let r <- self.item <- nil + return <-r! } } - pub fun hasStorageCapacity(_ addr: Address, _ storageFee: UFix64): Bool { + access(all) fun hasStorageCapacity(_ addr: Address, _ storageFee: UFix64): Bool { return FlowStorageFees.defaultTokenAvailableBalance(addr) > storageFee } - pub fun estimateDeposit( + access(all) fun estimateDeposit( item: @AnyResource, ): @DepositEstimate { - let storageUsedBefore = FeeEstimator.account.storageUsed - FeeEstimator.account.save(<-item, to: /storage/temp) - let storageUsedAfter = FeeEstimator.account.storageUsed + let storageUsedBefore = FeeEstimator.account.storage.used + FeeEstimator.account.storage.save(<-item, to: /storage/temp) + let storageUsedAfter = FeeEstimator.account.storage.used let storageDiff = storageUsedAfter - storageUsedBefore let storageFee = FeeEstimator.storageUsedToFlowAmount(storageDiff) - let loadedItem <- FeeEstimator.account.load<@AnyResource>(from: /storage/temp)! + let loadedItem <- FeeEstimator.account.storage.load<@AnyResource>(from: /storage/temp)! let estimate <- create DepositEstimate(item: <-loadedItem, storageFee: storageFee) return <- estimate } - pub fun storageUsedToFlowAmount(_ storageUsed: UInt64): UFix64 { + access(all) fun storageUsedToFlowAmount(_ storageUsed: UInt64): UFix64 { let storageMB = FlowStorageFees.convertUInt64StorageBytesToUFix64Megabytes(storageUsed) return FlowStorageFees.storageCapacityToFlow(storageMB) } diff --git a/contracts/lost-and-found/LostAndFound.cdc b/contracts/lost-and-found/LostAndFound.cdc index 456b58f..713b957 100644 --- a/contracts/lost-and-found/LostAndFound.cdc +++ b/contracts/lost-and-found/LostAndFound.cdc @@ -3,6 +3,7 @@ import "FlowStorageFees" import "FlowToken" import "NonFungibleToken" import "MetadataViews" + import "FeeEstimator" // LostAndFound @@ -26,49 +27,48 @@ import "FeeEstimator" // - NonFunigibleToken.Receiver // - FungibleToken.Receiver // - LostAndFound.ResourceReceiver (This is a placeholder so that non NFT and FT resources can be utilized here) -pub contract LostAndFound { +access(all) contract LostAndFound { access(contract) let storageFees: {UInt64: UFix64} - pub let LostAndFoundPublicPath: PublicPath - pub let LostAndFoundStoragePath: StoragePath - pub let DepositorPublicPath: PublicPath - pub let DepositorStoragePath: StoragePath + access(all) let LostAndFoundPublicPath: PublicPath + access(all) let LostAndFoundStoragePath: StoragePath + access(all) let DepositorPublicPath: PublicPath + access(all) let DepositorStoragePath: StoragePath + + access(all) event TicketDeposited(redeemer: Address, ticketID: UInt64, type: Type, memo: String?, name: String?, description: String?, thumbnail: String?) + access(all) event TicketRedeemed(redeemer: Address, ticketID: UInt64, type: Type) + access(all) event BinDestroyed(redeemer: Address, type: Type) + access(all) event ShelfDestroyed(redeemer: Address) + + access(all) event DepositorCreated(uuid: UInt64) + access(all) event DepositorBalanceLow(uuid: UInt64, threshold: UFix64, balance: UFix64) + access(all) event DepositorTokensAdded(uuid: UInt64, tokens: UFix64, balance: UFix64) + access(all) event DepositorTokensWithdrawn(uuid: UInt64, tokens: UFix64, balance: UFix64) - pub event TicketDeposited(redeemer: Address, ticketID: UInt64, type: Type, memo: String?, name: String?, description: String?, thumbnail: String?) - pub event TicketRedeemed(redeemer: Address, ticketID: UInt64, type: Type) - pub event BinDestroyed(redeemer: Address, type: Type) - pub event ShelfDestroyed(redeemer: Address) + // Used by the @Depositor resource and controls whether the depositor can be used + // or not to send resources to the LostAndFound + access(all) entitlement Deposit - pub event DepositorCreated(uuid: UInt64) - pub event DepositorBalanceLow(uuid: UInt64, threshold: UFix64, balance: UFix64) - pub event DepositorTokensAdded(uuid: UInt64, tokens: UFix64, balance: UFix64) - pub event DepositorTokensWithdrawn(uuid: UInt64, tokens: UFix64, balance: UFix64) + // Used by the @Depositor resource to manage settings such as low token threshold + access(all) entitlement Owner // Placeholder receiver so that any resource can be supported, not just FT and NFT Receivers - pub resource interface AnyResourceReceiver { - pub fun deposit(resource: @AnyResource) + access(all) resource interface AnyResourceReceiver { + access(Deposit) fun deposit(item: @AnyResource) } - pub resource DepositEstimate { - pub var item: @AnyResource? - pub let storageFee: UFix64 + access(all) resource DepositEstimate { + access(self) var item: @AnyResource? + access(all) let storageFee: UFix64 init(item: @AnyResource, storageFee: UFix64) { self.item <- item self.storageFee = storageFee } - pub fun withdraw(): @AnyResource { - let resource <- self.item <- nil - return <-resource! - } - - destroy() { - pre { - self.item == nil: "cannot destroy with non-null item" - } - - destroy self.item + access(all) fun withdraw(): @AnyResource { + let item <- self.item <- nil + return <-item! } } @@ -77,24 +77,30 @@ pub contract LostAndFound { // - memo: An optional message to attach to this ticket // - redeemer: The address which is allowed to withdraw the item from this ticket // - redeemed: Whether the ticket has been redeemed. This can only be set by the LostAndFound contract - pub resource Ticket { + access(all) resource Ticket { // The item to be redeemed access(contract) var item: @AnyResource? // An optional message to attach to this item. - pub let memo: String? + access(all) let memo: String? // an optional Display view so that frontend's that borrow this ticket know how to show it - pub let display: MetadataViews.Display? + access(all) let display: MetadataViews.Display? // The address that it allowed to withdraw the item fromt this ticket - pub let redeemer: Address + access(all) let redeemer: Address //The type of the resource (non-optional) so that bins can represent the true type of an item - pub let type: Type + access(all) let type: Type // State maintained by LostAndFound - pub var redeemed: Bool + access(all) var redeemed: Bool // flow token amount used to store this ticket is returned when the ticket is redeemed - access(contract) let flowTokenRepayment: Capability<&FlowToken.Vault{FungibleToken.Receiver}>? + access(contract) let flowTokenRepayment: Capability<&FlowToken.Vault>? - init (item: @AnyResource, memo: String?, display: MetadataViews.Display?, redeemer: Address, flowTokenRepayment: Capability<&FlowToken.Vault{FungibleToken.Receiver}>?) { + init ( + item: @AnyResource, + memo: String?, + display: MetadataViews.Display?, + redeemer: Address, + flowTokenRepayment: Capability<&FlowToken.Vault>? + ) { self.type = item.getType() self.item <- item self.memo = memo @@ -105,40 +111,40 @@ pub contract LostAndFound { self.flowTokenRepayment = flowTokenRepayment } - pub fun itemType(): Type { + access(all) view fun itemType(): Type { return self.type } - pub fun checkItem(): Bool { + access(all) fun checkItem(): Bool { return self.item != nil } // A function to get depositor address / flow Repayment address - pub fun getFlowRepaymentAddress() : Address? { + access(all) fun getFlowRepaymentAddress() : Address? { return self.flowTokenRepayment?.address } // If this is an instance of NFT, return the id , otherwise return nil - pub fun getNonFungibleTokenID() : UInt64? { - if self.type.isSubtype(of: Type<@NonFungibleToken.NFT>()) { - let ref = (&self.item as auth &AnyResource?)! - let nft = ref as! &NonFungibleToken.NFT + access(all) fun getNonFungibleTokenID() : UInt64? { + if self.type.isSubtype(of: Type<@{NonFungibleToken.NFT}>()) { + let ref = (&self.item as &AnyResource?)! + let nft = ref as! &{NonFungibleToken.NFT} return nft.id } return nil } // If this is an instance of FT, return the vault balance , otherwise return nil - pub fun getFungibleTokenBalance() : UFix64? { - if self.type.isSubtype(of: Type<@FungibleToken.Vault>()) { - let ref = (&self.item as auth &AnyResource?)! - let ft = ref as! &FungibleToken.Vault + access(all) fun getFungibleTokenBalance() : UFix64? { + if self.type.isSubtype(of: Type<@{FungibleToken.Vault}>()) { + let ref = (&self.item as &AnyResource?)! + let ft = ref as! &{FungibleToken.Vault} return ft.balance } return nil } - pub fun withdraw(receiver: Capability) { + access(contract) fun withdraw(receiver: Capability) { pre { receiver.address == self.redeemer: "receiver address and redeemer must match" !self.redeemed: "already redeemed" @@ -147,25 +153,25 @@ pub contract LostAndFound { var redeemableItem <- self.item <- nil let cap = receiver.borrow<&AnyResource>()! - if cap.isInstance(Type<@NonFungibleToken.Collection>()) { - let target = receiver.borrow<&AnyResource{NonFungibleToken.CollectionPublic}>()! - let token <- redeemableItem as! @NonFungibleToken.NFT? + if cap.isInstance(Type<@{NonFungibleToken.CollectionPublic}>()) { + let target = receiver.borrow<&{NonFungibleToken.CollectionPublic}>()! + let token <- redeemableItem as! @{NonFungibleToken.NFT}? self.redeemed = true emit TicketRedeemed(redeemer: self.redeemer, ticketID: self.uuid, type: token.getType()) target.deposit(token: <- token!) return - } else if cap.isInstance(Type<@FungibleToken.Vault>()) { - let target = receiver.borrow<&AnyResource{FungibleToken.Receiver}>()! - let token <- redeemableItem as! @FungibleToken.Vault? + } else if cap.isInstance(Type<@{FungibleToken.Vault}>()) { + let target = receiver.borrow<&{FungibleToken.Receiver}>()! + let token <- redeemableItem as! @{FungibleToken.Vault}? self.redeemed = true emit TicketRedeemed(redeemer: self.redeemer, ticketID: self.uuid, type: token.getType()) target.deposit(from: <- token!) return - } else if cap.isInstance(Type<@AnyResource{LostAndFound.AnyResourceReceiver}>()) { - let target = receiver.borrow<&{LostAndFound.AnyResourceReceiver}>()! + } else if cap.isInstance(Type<@{LostAndFound.AnyResourceReceiver}>()) { + let target = receiver.borrow()! self.redeemed = true emit TicketRedeemed(redeemer: self.redeemer, ticketID: self.uuid, type: redeemableItem.getType()) - target.deposit(resource: <- redeemableItem) + target.deposit(item: <- redeemableItem) return } else{ panic("cannot redeem resource to receiver") @@ -180,15 +186,13 @@ pub contract LostAndFound { return <-redeemableItem! } - // destructon is only allowed if the ticket has been redeemed and the underlying item is a our dummy resource - destroy () { + access(contract) fun safeDestroy() { pre { self.redeemed: "Ticket has not been redeemed" self.item == nil: "can only destroy if not holding any item" } LostAndFound.storageFees.remove(key: self.uuid) - destroy <-self.item } } @@ -196,11 +200,11 @@ pub contract LostAndFound { // A Bin is a resource that gathers tickets whos item have the same type. // For instance, if two TopShot Moments are deposited to the same redeemer, only one bin // will be made which will contain both tickets to redeem each individual moment. - pub resource Bin { + access(all) resource Bin { access(contract) let tickets: @{UInt64:Ticket} access(contract) let type: Type - pub let flowTokenRepayment: Capability<&{FungibleToken.Receiver}>? + access(all) let flowTokenRepayment: Capability<&{FungibleToken.Receiver}>? init (type: Type, flowTokenRepayment: Capability<&{FungibleToken.Receiver}>?) { self.tickets <- {} @@ -208,11 +212,11 @@ pub contract LostAndFound { self.flowTokenRepayment = flowTokenRepayment } - pub fun borrowTicket(id: UInt64): &LostAndFound.Ticket? { - return &self.tickets[id] as &LostAndFound.Ticket? + access(all) fun borrowTicket(id: UInt64): &LostAndFound.Ticket? { + return &self.tickets[id] } - pub fun borrowAllTicketsByType(): [&LostAndFound.Ticket] { + access(all) fun borrowAllTicketsByType(): [&LostAndFound.Ticket] { let tickets: [&LostAndFound.Ticket] = [] let ids = self.tickets.keys for id in ids { @@ -243,7 +247,7 @@ pub contract LostAndFound { emit TicketDeposited(redeemer: redeemer, ticketID: ticketID, type: self.type, memo: memo, name: name, description: description, thumbnail: thumbnail) } - pub fun getTicketIDs(): [UInt64] { + access(all) fun getTicketIDs(): [UInt64] { return self.tickets.keys } @@ -252,16 +256,17 @@ pub contract LostAndFound { return <- ticket! } - destroy () { - destroy <-self.tickets - LostAndFound.storageFees.remove(key: self.uuid) + access(contract) fun safeDestroy() { + pre { + self.tickets.length == 0: "cannot destroy bin with tickets in it" + } } } // A shelf is our top-level organization resource. // It groups bins by type to help make discovery of the assets that a // redeeming address can claim. - pub resource Shelf { + access(all) resource Shelf { access(self) let bins: @{String: Bin} access(self) let identifierToType: {String: Type} access(self) let redeemer: Address @@ -274,11 +279,11 @@ pub contract LostAndFound { self.flowTokenRepayment = flowTokenRepayment } - pub fun getOwner(): Address { + access(all) fun getOwner(): Address { return self.owner!.address } - pub fun getRedeemableTypes(): [Type] { + access(all) fun getRedeemableTypes(): [Type] { let types: [Type] = [] for k in self.bins.keys { let t = self.identifierToType[k]! @@ -289,26 +294,26 @@ pub contract LostAndFound { return types } - pub fun hasType(type: Type): Bool { + access(all) fun hasType(type: Type): Bool { return self.bins[type.identifier] != nil } - pub fun borrowBin(type: Type): &LostAndFound.Bin? { - return &self.bins[type.identifier] as &LostAndFound.Bin? + access(all) fun borrowBin(type: Type): &LostAndFound.Bin? { + return &self.bins[type.identifier] } access(contract) fun ensureBin(type: Type, flowTokenRepayment: Capability<&{FungibleToken.Receiver}>?): &Bin { if !self.bins.containsKey(type.identifier) { - let storageBefore = LostAndFound.account.storageUsed + let storageBefore = LostAndFound.account.storage.used let bin <- create Bin(type: type, flowTokenRepayment: flowTokenRepayment) let uuid = bin.uuid let oldValue <- self.bins.insert(key: type.identifier, <-bin) - LostAndFound.storageFees[uuid] = FeeEstimator.storageUsedToFlowAmount(LostAndFound.account.storageUsed - storageBefore) + LostAndFound.storageFees[uuid] = FeeEstimator.storageUsedToFlowAmount(LostAndFound.account.storage.used - storageBefore) self.identifierToType[type.identifier] = type destroy oldValue } - return (&self.bins[type.identifier] as &LostAndFound.Bin?)! + return (&self.bins[type.identifier])! } access(contract) fun deposit(ticket: @LostAndFound.Ticket, flowTokenRepayment: Capability<&{FungibleToken.Receiver}>?) { @@ -324,7 +329,7 @@ pub contract LostAndFound { // Only one of the three receiver options can be specified, and an optional maximum number of tickets // to redeem can be picked to prevent gas issues in case there are large numbers of tickets to be // redeemed at once. - pub fun redeemAll(type: Type, max: Int?, receiver: Capability) { + access(all) fun redeemAll(type: Type, max: Int?, receiver: Capability) { pre { receiver.address == self.redeemer: "receiver must match the redeemer of this shelf" self.bins.containsKey(type.identifier): "no bin for provided type" @@ -343,7 +348,7 @@ pub contract LostAndFound { } // Redeem a specific ticket instead of all of a certain type. - pub fun redeem(type: Type, ticketID: UInt64, receiver: Capability) { + access(all) fun redeem(type: Type, ticketID: UInt64, receiver: Capability) { pre { receiver.address == self.redeemer: "receiver must match the redeemer of this shelf" self.bins.containsKey(type.identifier): "no bin for provided type" @@ -360,6 +365,8 @@ pub contract LostAndFound { let repaymentVault <- refundProvider.withdraw(amount: LostAndFound.storageFees[uuid]!) refundCap!.borrow()!.deposit(from: <-repaymentVault) } + + ticket.safeDestroy() destroy ticket if borrowedBin.getTicketIDs().length == 0 { @@ -374,77 +381,81 @@ pub contract LostAndFound { let vault <- provider.withdraw(amount: LostAndFound.storageFees[uuid]!) flowTokenRepayment!.borrow()!.deposit(from: <-vault) } + + bin.safeDestroy() destroy bin } } - destroy () { - destroy <- self.bins + access(contract) fun safeDestroy() { + pre { + self.bins.length == 0: "cannot destroy a shelf with bins in it" + } LostAndFound.storageFees.remove(key: self.uuid) } } - access(contract) fun getFlowProvider(): &FlowToken.Vault{FungibleToken.Provider} { - return self.account.borrow<&FlowToken.Vault{FungibleToken.Provider}>(from: /storage/flowTokenVault)! + access(contract) fun getFlowProvider(): auth(FungibleToken.Withdraw) &FlowToken.Vault { + return self.account.storage.borrow(from: /storage/flowTokenVault)! } // ShelfManager is a light-weight wrapper to get our shelves into storage. - pub resource ShelfManager { + access(all) resource ShelfManager { access(self) let shelves: @{Address: Shelf} init() { self.shelves <- {} } - access(contract) fun ensureShelf(_ addr: Address, flowTokenRepayment: Capability<&FlowToken.Vault{FungibleToken.Receiver}>?): &LostAndFound.Shelf { + access(contract) fun ensureShelf(_ addr: Address, flowTokenRepayment: Capability<&FlowToken.Vault>?): &LostAndFound.Shelf { if !self.shelves.containsKey(addr) { - let storageBefore = LostAndFound.account.storageUsed + let storageBefore = LostAndFound.account.storage.used let shelf <- create Shelf(redeemer: addr, flowTokenRepayment: flowTokenRepayment) let uuid = shelf.uuid let oldValue <- self.shelves.insert(key: addr, <-shelf) - LostAndFound.storageFees[uuid] = FeeEstimator.storageUsedToFlowAmount(LostAndFound.account.storageUsed - storageBefore) + LostAndFound.storageFees[uuid] = FeeEstimator.storageUsedToFlowAmount(LostAndFound.account.storage.used - storageBefore) destroy oldValue } - return (&self.shelves[addr] as &LostAndFound.Shelf?)! + return (&self.shelves[addr])! } - pub fun deposit( + access(all) fun deposit( redeemer: Address, item: @AnyResource, memo: String?, display: MetadataViews.Display?, - storagePayment: &FungibleToken.Vault, - flowTokenRepayment: Capability<&FlowToken.Vault{FungibleToken.Receiver}>? + storagePayment: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}, + flowTokenRepayment: Capability<&FlowToken.Vault>? ) : UInt64 { pre { flowTokenRepayment == nil || flowTokenRepayment!.check(): "flowTokenRepayment is not valid" storagePayment.getType() == Type<@FlowToken.Vault>(): "storage payment must be in flow tokens" } let receiver = LostAndFound.account - .getCapability<&FlowToken.Vault{FungibleToken.Receiver}>(/public/flowTokenReceiver) + .capabilities.get<&FlowToken.Vault>(/public/flowTokenReceiver) .borrow()! - let storageBeforeShelf = LostAndFound.account.storageUsed + let storageBeforeShelf = LostAndFound.account.storage.used let shelf = self.ensureShelf(redeemer, flowTokenRepayment: flowTokenRepayment) - if LostAndFound.account.storageUsed != storageBeforeShelf && LostAndFound.storageFees[shelf.uuid] != nil { + if LostAndFound.account.storage.used != storageBeforeShelf && LostAndFound.storageFees[shelf.uuid] != nil { receiver.deposit(from: <-storagePayment.withdraw(amount: LostAndFound.storageFees[shelf.uuid]!)) } - let storageBeforeBin = LostAndFound.account.storageUsed + let storageBeforeBin = LostAndFound.account.storage.used let bin = shelf.ensureBin(type: item.getType(), flowTokenRepayment: flowTokenRepayment) - if LostAndFound.account.storageUsed != storageBeforeBin { + if LostAndFound.account.storage.used != storageBeforeBin { receiver.deposit(from: <-storagePayment.withdraw(amount: LostAndFound.storageFees[bin.uuid]!)) } - let storageBefore = LostAndFound.account.storageUsed + let storageBefore = LostAndFound.account.storage.used let ticket <- create Ticket(item: <-item, memo: memo, display: display, redeemer: redeemer, flowTokenRepayment: flowTokenRepayment) let uuid = ticket.uuid let flowTokenRepayment = ticket.flowTokenRepayment shelf.deposit(ticket: <-ticket, flowTokenRepayment: flowTokenRepayment) - let storageUsedAfter = LostAndFound.account.storageUsed + let storageUsedAfter = LostAndFound.account.storage.used let storageFee = FeeEstimator.storageUsedToFlowAmount(storageUsedAfter - storageBefore) LostAndFound.storageFees[uuid] = storageFee @@ -453,20 +464,20 @@ pub contract LostAndFound { return uuid } - pub fun borrowShelf(redeemer: Address): &LostAndFound.Shelf? { - return &self.shelves[redeemer] as &LostAndFound.Shelf? + access(all) fun borrowShelf(redeemer: Address): &LostAndFound.Shelf? { + return &self.shelves[redeemer] } // deleteShelf // // delete a shelf if it has no redeemable types - pub fun deleteShelf(_ addr: Address) { - let storageBefore = LostAndFound.account.storageUsed + access(all) fun deleteShelf(_ addr: Address) { + let storageBefore = LostAndFound.account.storage.used assert(self.shelves.containsKey(addr), message: "shelf does not exist") let tmp <- self.shelves[addr] <- nil let shelf <-! tmp! - assert(shelf.getRedeemableTypes().length! == 0, message: "shelf still has redeemable types") + assert(shelf.getRedeemableTypes().length == 0, message: "shelf still has redeemable types") let flowTokenRepayment = shelf.flowTokenRepayment let uuid = shelf.uuid if flowTokenRepayment != nil && flowTokenRepayment!.check() && LostAndFound.storageFees[uuid] != nil { @@ -474,23 +485,27 @@ pub contract LostAndFound { let vault <- provider.withdraw(amount: LostAndFound.storageFees[uuid]!) flowTokenRepayment!.borrow()!.deposit(from: <-vault) } + + shelf.safeDestroy() destroy shelf emit ShelfDestroyed(redeemer: addr) } - - destroy () { - destroy <-self.shelves + + access(contract) fun safeDestroy() { + pre { + self.shelves.length == 0: "cannot destroy shelf manager when it has shelves" + } } } - pub resource interface DepositorPublic { - pub fun balance(): UFix64 - pub fun addFlowTokens(vault: @FlowToken.Vault) + access(all) resource interface DepositorPublic { + access(all) fun balance(): UFix64 + access(all) fun addFlowTokens(vault: @FlowToken.Vault) } - pub resource Depositor: DepositorPublic { + access(all) resource Depositor: DepositorPublic { access(self) let flowTokenVault: @FlowToken.Vault - pub let flowTokenRepayment: Capability<&FlowToken.Vault{FungibleToken.Receiver}> + access(all) let flowTokenRepayment: Capability<&FlowToken.Vault> access(self) var lowBalanceThreshold: UFix64? access(self) fun checkForLowBalance(): Bool { @@ -502,45 +517,45 @@ pub contract LostAndFound { return false } - pub fun setLowBalanceThreshold(threshold: UFix64?) { + access(Owner) fun setLowBalanceThreshold(threshold: UFix64?) { self.lowBalanceThreshold = threshold } - pub fun getLowBalanceThreshold(): UFix64? { + access(Owner) fun getLowBalanceThreshold(): UFix64? { return self.lowBalanceThreshold } - pub fun deposit( + access(Deposit) fun deposit( redeemer: Address, item: @AnyResource, memo: String?, display: MetadataViews.Display? ) : UInt64 { let receiver = LostAndFound.account - .getCapability<&FlowToken.Vault{FungibleToken.Receiver}>(/public/flowTokenReceiver) + .capabilities.get<&FlowToken.Vault>(/public/flowTokenReceiver) .borrow()! - let storageBeforeShelf = LostAndFound.account.storageUsed + let storageBeforeShelf = LostAndFound.account.storage.used let shelfManager = LostAndFound.borrowShelfManager() let shelf = shelfManager.ensureShelf(redeemer, flowTokenRepayment: self.flowTokenRepayment) - if LostAndFound.account.storageUsed != storageBeforeShelf && LostAndFound.storageFees[shelf.uuid] != nil { + if LostAndFound.account.storage.used != storageBeforeShelf && LostAndFound.storageFees[shelf.uuid] != nil { receiver.deposit(from: <-self.withdrawTokens(amount: LostAndFound.storageFees[shelf.uuid]!)) } - let storageBeforeBin = LostAndFound.account.storageUsed + let storageBeforeBin = LostAndFound.account.storage.used let bin = shelf.ensureBin(type: item.getType(), flowTokenRepayment: self.flowTokenRepayment) - if storageBeforeBin != LostAndFound.account.storageUsed { + if storageBeforeBin != LostAndFound.account.storage.used { receiver.deposit(from: <-self.withdrawTokens(amount: LostAndFound.storageFees[bin.uuid]!)) } - let storageBefore = LostAndFound.account.storageUsed + let storageBefore = LostAndFound.account.storage.used let ticket <- create Ticket(item: <-item, memo: memo, display: display, redeemer: redeemer, flowTokenRepayment: self.flowTokenRepayment) let flowTokenRepayment = ticket.flowTokenRepayment let uuid = ticket.uuid - shelf!.deposit(ticket: <-ticket, flowTokenRepayment: flowTokenRepayment) + shelf.deposit(ticket: <-ticket, flowTokenRepayment: flowTokenRepayment) - let storageFee = FeeEstimator.storageUsedToFlowAmount(LostAndFound.account.storageUsed - storageBefore) + let storageFee = FeeEstimator.storageUsedToFlowAmount(LostAndFound.account.storage.used - storageBefore) LostAndFound.storageFees[uuid] = storageFee let storagePaymentVault <- self.withdrawTokens(amount: storageFee) @@ -549,69 +564,67 @@ pub contract LostAndFound { return uuid } - pub fun trySendResource( - item: @AnyResource, - cap: Capability, - memo: String?, - display: MetadataViews.Display? + access(Deposit) fun trySendResource( + item: @AnyResource, + cap: Capability, + memo: String?, + display: MetadataViews.Display? ) { - if cap.check<&{NonFungibleToken.CollectionPublic}>() { - let nft <- item as! @NonFungibleToken.NFT + let nft <- item as! @{NonFungibleToken.NFT} cap.borrow<&{NonFungibleToken.CollectionPublic}>()!.deposit(token: <-nft) - } else if cap.check<&{NonFungibleToken.Receiver}>() { - let nft <- item as! @NonFungibleToken.NFT - cap.borrow<&{NonFungibleToken.Receiver}>()!.deposit(token: <-nft) } else if cap.check<&{FungibleToken.Receiver}>() { - let vault <- item as! @FungibleToken.Vault + let vault <- item as! @{FungibleToken.Vault} cap.borrow<&{FungibleToken.Receiver}>()!.deposit(from: <-vault) } else { self.deposit(redeemer: cap.address, item: <-item, memo: memo, display: display) } } - pub fun withdrawTokens(amount: UFix64): @FungibleToken.Vault { + access(Owner) fun withdrawTokens(amount: UFix64): @{FungibleToken.Vault} { let tokens <-self.flowTokenVault.withdraw(amount: amount) emit DepositorTokensWithdrawn(uuid: self.uuid, tokens: amount, balance: self.flowTokenVault.balance) self.checkForLowBalance() return <-tokens } - pub fun addFlowTokens(vault: @FlowToken.Vault) { + access(all) fun addFlowTokens(vault: @FlowToken.Vault) { let tokensAdded = vault.balance self.flowTokenVault.deposit(from: <-vault) emit DepositorTokensAdded(uuid: self.uuid, tokens: tokensAdded, balance: self.flowTokenVault.balance) self.checkForLowBalance() } - pub fun balance(): UFix64 { + access(all) fun balance(): UFix64 { return self.flowTokenVault.balance } - init(_ flowTokenRepayment: Capability<&FlowToken.Vault{FungibleToken.Receiver}>, lowBalanceThreshold: UFix64?) { + init(_ flowTokenRepayment: Capability<&FlowToken.Vault>, lowBalanceThreshold: UFix64?) { self.flowTokenRepayment = flowTokenRepayment - let vault <- FlowToken.createEmptyVault() - self.flowTokenVault <- vault as! @FlowToken.Vault + let vault <- FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>()) + self.flowTokenVault <- vault self.lowBalanceThreshold = lowBalanceThreshold } - destroy() { - self.flowTokenRepayment.borrow()!.deposit(from: <-self.flowTokenVault) + access(contract) fun safeDestroy() { + pre { + self.flowTokenVault.balance == 0.0: "depositor still has flow tokens to be withdrawn" + } } } - pub fun createDepositor(_ flowTokenRepayment: Capability<&FlowToken.Vault{FungibleToken.Receiver}>, lowBalanceThreshold: UFix64?): @Depositor { + access(all) fun createDepositor(_ flowTokenRepayment: Capability<&FlowToken.Vault>, lowBalanceThreshold: UFix64?): @Depositor { let depositor <- create Depositor(flowTokenRepayment, lowBalanceThreshold: lowBalanceThreshold) emit DepositorCreated(uuid: depositor.uuid) return <- depositor } - pub fun borrowShelfManager(): &LostAndFound.ShelfManager { - return self.account.getCapability<&LostAndFound.ShelfManager>(LostAndFound.LostAndFoundPublicPath).borrow()! + access(all) fun borrowShelfManager(): &LostAndFound.ShelfManager { + return self.account.capabilities.get<&LostAndFound.ShelfManager>(LostAndFound.LostAndFoundPublicPath).borrow()! } - pub fun borrowAllTicketsByType(addr: Address, type: Type): [&LostAndFound.Ticket] { + access(all) fun borrowAllTicketsByType(addr: Address, type: Type): [&LostAndFound.Ticket] { let manager = LostAndFound.borrowShelfManager() let shelf = manager.borrowShelf(redeemer: addr) if shelf == nil { @@ -626,7 +639,7 @@ pub contract LostAndFound { return bin!.borrowAllTicketsByType() } - pub fun borrowAllTickets(addr: Address): [&LostAndFound.Ticket] { + access(all) fun borrowAllTickets(addr: Address): [&LostAndFound.Ticket] { let manager = LostAndFound.borrowShelfManager() let shelf = manager.borrowShelf(redeemer: addr) if shelf == nil { @@ -634,7 +647,7 @@ pub contract LostAndFound { } let types = shelf!.getRedeemableTypes() - let allTickets = [] as [&LostAndFound.Ticket] + let allTickets: [&Ticket] = [] for type in types { let tickets = LostAndFound.borrowAllTicketsByType(addr: addr, type: type) @@ -644,19 +657,19 @@ pub contract LostAndFound { return allTickets } - pub fun redeemAll(type: Type, max: Int?, receiver: Capability) { + access(all) fun redeemAll(type: Type, max: Int?, receiver: Capability) { let manager = LostAndFound.borrowShelfManager() - let shelf = manager.borrowShelf(redeemer: receiver.address) + let shelf = manager.borrowShelf(redeemer: receiver.address)! assert(shelf != nil, message: "shelf not found") - shelf!.redeemAll(type: type, max: max, receiver: receiver) - let remainingTypes = shelf!.getRedeemableTypes() + shelf.redeemAll(type: type, max: max, receiver: receiver) + let remainingTypes = shelf.getRedeemableTypes() if remainingTypes.length == 0 { manager.deleteShelf(receiver.address) } } - pub fun estimateDeposit( + access(all) fun estimateDeposit( redeemer: Address, item: @AnyResource, memo: String?, @@ -677,7 +690,7 @@ pub contract LostAndFound { } } - let ftReceiver = LostAndFound.account.getCapability<&FlowToken.Vault{FungibleToken.Receiver}>(/public/flowTokenReceiver) + let ftReceiver = LostAndFound.account.capabilities.get<&FlowToken.Vault>(/public/flowTokenReceiver) let ticket <- create LostAndFound.Ticket(item: <-item, memo: memo, display: display, redeemer: redeemer, flowTokenRepayment: ftReceiver) let tmpEstimate <- FeeEstimator.estimateDeposit(item: <-ticket) let tmpItem <- tmpEstimate.withdraw() as! @LostAndFound.Ticket @@ -689,7 +702,7 @@ pub contract LostAndFound { return <- estimate } - pub fun getRedeemableTypes(_ addr: Address): [Type] { + access(all) fun getRedeemableTypes(_ addr: Address): [Type] { let manager = LostAndFound.borrowShelfManager() let shelf = manager.borrowShelf(redeemer: addr) if shelf == nil { @@ -699,13 +712,13 @@ pub contract LostAndFound { return shelf!.getRedeemableTypes() } - pub fun deposit( + access(all) fun deposit( redeemer: Address, item: @AnyResource, memo: String?, display: MetadataViews.Display?, - storagePayment: &FungibleToken.Vault, - flowTokenRepayment: Capability<&FlowToken.Vault{FungibleToken.Receiver}>? + storagePayment: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}, + flowTokenRepayment: Capability<&FlowToken.Vault>? ) : UInt64 { pre { flowTokenRepayment == nil || flowTokenRepayment!.check(): "flowTokenRepayment is not valid" @@ -716,29 +729,26 @@ pub contract LostAndFound { return shelfManager.deposit(redeemer: redeemer, item: <-item, memo: memo, display: display, storagePayment: storagePayment, flowTokenRepayment: flowTokenRepayment) } - pub fun trySendResource( - resource: @AnyResource, + access(all) fun trySendResource( + item: @AnyResource, cap: Capability, memo: String?, display: MetadataViews.Display?, - storagePayment: &FungibleToken.Vault, - flowTokenRepayment: Capability<&FlowToken.Vault{FungibleToken.Receiver}> + storagePayment: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}, + flowTokenRepayment: Capability<&FlowToken.Vault> ) { if cap.check<&{NonFungibleToken.CollectionPublic}>() { - let nft <- resource as! @NonFungibleToken.NFT + let nft <- item as! @{NonFungibleToken.NFT} cap.borrow<&{NonFungibleToken.CollectionPublic}>()!.deposit(token: <-nft) - } else if cap.check<&{NonFungibleToken.Receiver}>() { - let nft <- resource as! @NonFungibleToken.NFT - cap.borrow<&{NonFungibleToken.Receiver}>()!.deposit(token: <-nft) } else if cap.check<&{FungibleToken.Receiver}>() { - let vault <- resource as! @FungibleToken.Vault + let vault <- item as! @{FungibleToken.Vault} cap.borrow<&{FungibleToken.Receiver}>()!.deposit(from: <-vault) } else { - LostAndFound.deposit(redeemer: cap.address, item: <-resource, memo: memo, display: display, storagePayment: storagePayment, flowTokenRepayment: flowTokenRepayment) + LostAndFound.deposit(redeemer: cap.address, item: <-item, memo: memo, display: display, storagePayment: storagePayment, flowTokenRepayment: flowTokenRepayment) } } - pub fun getAddress(): Address { + access(all) fun getAddress(): Address { return self.account.address } @@ -751,8 +761,10 @@ pub contract LostAndFound { self.DepositorStoragePath = /storage/lostAndFoundDepositor let manager <- create ShelfManager() - self.account.save(<-manager, to: self.LostAndFoundStoragePath) - self.account.link<&LostAndFound.ShelfManager>(self.LostAndFoundPublicPath, target: self.LostAndFoundStoragePath) + self.account.storage.save(<-manager, to: self.LostAndFoundStoragePath) + + let cap = self.account.capabilities.storage.issue<&LostAndFound.ShelfManager>(self.LostAndFoundStoragePath) + self.account.capabilities.publish(cap, at: self.LostAndFoundPublicPath) } } \ No newline at end of file diff --git a/contracts/lost-and-found/LostAndFoundHelper.cdc b/contracts/lost-and-found/LostAndFoundHelper.cdc index 4148337..7476a0b 100644 --- a/contracts/lost-and-found/LostAndFoundHelper.cdc +++ b/contracts/lost-and-found/LostAndFoundHelper.cdc @@ -1,22 +1,22 @@ import "LostAndFound" -pub contract LostAndFoundHelper { +access(all) contract LostAndFoundHelper { - pub struct Ticket { + access(all) struct Ticket { // An optional message to attach to this item. - pub let memo: String? + access(all) let memo: String? // The address that it allowed to withdraw the item fromt this ticket - pub let redeemer: Address + access(all) let redeemer: Address //The type of the resource (non-optional) so that bins can represent the true type of an item - pub let type: Type - pub let typeIdentifier: String + access(all) let type: Type + access(all) let typeIdentifier: String // State maintained by LostAndFound - pub let redeemed: Bool - pub let name : String? - pub let description : String? - pub let thumbnail : String? - pub let ticketID : UInt64? + access(all) let redeemed: Bool + access(all) let name : String? + access(all) let description : String? + access(all) let thumbnail : String? + access(all) let ticketID : UInt64? init(_ ticket: &LostAndFound.Ticket, id: UInt64?) { self.memo = ticket.memo @@ -32,7 +32,7 @@ pub contract LostAndFoundHelper { } - pub fun constructResult(_ ticket: &LostAndFound.Ticket?, id:UInt64?) : LostAndFoundHelper.Ticket? { + access(all) fun constructResult(_ ticket: &LostAndFound.Ticket?, id:UInt64?) : LostAndFoundHelper.Ticket? { if ticket != nil { return LostAndFoundHelper.Ticket(ticket!, id: id) } diff --git a/contracts/nft-catalog/NFTCatalog.cdc b/contracts/nft-catalog/NFTCatalog.cdc index f864f72..6dfa4f7 100644 --- a/contracts/nft-catalog/NFTCatalog.cdc +++ b/contracts/nft-catalog/NFTCatalog.cdc @@ -10,10 +10,10 @@ import "MetadataViews" // To make an addition to the catalog you can propose an NFT and provide its metadata. // An Admin can approve a proposal which would add the NFT to the catalog -pub contract NFTCatalog { +access(all) contract NFTCatalog { // EntryAdded // An NFT collection has been added to the catalog - pub event EntryAdded( + access(all) event EntryAdded( collectionIdentifier : String, contractName : String, contractAddress : Address, @@ -30,7 +30,7 @@ pub contract NFTCatalog { // EntryUpdated // An NFT Collection has been updated in the catalog - pub event EntryUpdated( + access(all) event EntryUpdated( collectionIdentifier : String, contractName : String, contractAddress : Address, @@ -47,23 +47,23 @@ pub contract NFTCatalog { // EntryRemoved // An NFT Collection has been removed from the catalog - pub event EntryRemoved(collectionIdentifier : String, nftType: Type) + access(all) event EntryRemoved(collectionIdentifier : String, nftType: Type) // ProposalEntryAdded // A new proposal to make an addtion to the catalog has been made - pub event ProposalEntryAdded(proposalID : UInt64, collectionIdentifier : String, message: String, status: String, proposer : Address) + access(all) event ProposalEntryAdded(proposalID : UInt64, collectionIdentifier : String, message: String, status: String, proposer : Address) // ProposalEntryUpdated // A proposal has been updated - pub event ProposalEntryUpdated(proposalID : UInt64, collectionIdentifier : String, message: String, status: String, proposer : Address) + access(all) event ProposalEntryUpdated(proposalID : UInt64, collectionIdentifier : String, message: String, status: String, proposer : Address) // ProposalEntryRemoved // A proposal has been removed from storage - pub event ProposalEntryRemoved(proposalID : UInt64) + access(all) event ProposalEntryRemoved(proposalID : UInt64) - pub let ProposalManagerStoragePath: StoragePath + access(all) let ProposalManagerStoragePath: StoragePath - pub let ProposalManagerPublicPath: PublicPath + access(all) let ProposalManagerPublicPath: PublicPath access(self) let catalog: {String : NFTCatalog.NFTCatalogMetadata} // { collectionIdentifier -> Metadata } access(self) let catalogTypeData: {String : {String : Bool}} // Additional view to go from { NFT Type Identifier -> {Collection Identifier : Bool } } @@ -75,17 +75,17 @@ pub contract NFTCatalog { // NFTCatalogProposalManager // Used to authenticate proposals made to the catalog - pub resource interface NFTCatalogProposalManagerPublic { - pub fun getCurrentProposalEntry(): String? + access(all) resource interface NFTCatalogProposalManagerPublic { + access(all) fun getCurrentProposalEntry(): String? } - pub resource NFTCatalogProposalManager : NFTCatalogProposalManagerPublic { + access(all) resource NFTCatalogProposalManager : NFTCatalogProposalManagerPublic { access(self) var currentProposalEntry: String? - pub fun getCurrentProposalEntry(): String? { + access(all) fun getCurrentProposalEntry(): String? { return self.currentProposalEntry } - pub fun setCurrentProposalEntry(identifier: String?) { + access(all) fun setCurrentProposalEntry(identifier: String?) { self.currentProposalEntry = identifier } @@ -95,19 +95,19 @@ pub contract NFTCatalog { } - pub resource Snapshot { - pub var catalogSnapshot: {String : NFTCatalogMetadata} - pub var shouldUseSnapshot: Bool + access(all) resource Snapshot { + access(all) var catalogSnapshot: {String : NFTCatalogMetadata} + access(all) var shouldUseSnapshot: Bool - pub fun setPartialSnapshot(_ snapshotKey: String, _ snapshotEntry: NFTCatalogMetadata) { + access(all) fun setPartialSnapshot(_ snapshotKey: String, _ snapshotEntry: NFTCatalogMetadata) { self.catalogSnapshot[snapshotKey] = snapshotEntry } - pub fun setShouldUseSnapshot(_ shouldUseSnapshot: Bool) { + access(all) fun setShouldUseSnapshot(_ shouldUseSnapshot: Bool) { self.shouldUseSnapshot = shouldUseSnapshot } - pub fun getCatalogSnapshot(): {String : NFTCatalogMetadata} { + access(all) fun getCatalogSnapshot(): {String : NFTCatalogMetadata} { return self.catalogSnapshot } @@ -117,7 +117,7 @@ pub contract NFTCatalog { } } - pub fun createEmptySnapshot(): @Snapshot { + access(all) fun createEmptySnapshot(): @Snapshot { return <- create Snapshot() } @@ -125,13 +125,13 @@ pub contract NFTCatalog { // Represents information about an NFT collection resource // Note: Not suing the struct from Metadata standard due to // inability to store functions - pub struct NFTCollectionData { + access(all) struct NFTCollectionData { - pub let storagePath : StoragePath - pub let publicPath : PublicPath - pub let privatePath: PrivatePath - pub let publicLinkedType: Type - pub let privateLinkedType: Type + access(all) let storagePath : StoragePath + access(all) let publicPath : PublicPath + access(all) let privatePath: PrivatePath + access(all) let publicLinkedType: Type + access(all) let privateLinkedType: Type init( storagePath : StoragePath, @@ -150,12 +150,12 @@ pub contract NFTCatalog { // NFTCatalogMetadata // Represents data about an NFT - pub struct NFTCatalogMetadata { - pub let contractName : String - pub let contractAddress : Address - pub let nftType: Type - pub let collectionData: NFTCollectionData - pub let collectionDisplay: MetadataViews.NFTCollectionDisplay + access(all) struct NFTCatalogMetadata { + access(all) let contractName : String + access(all) let contractAddress : Address + access(all) let nftType: Type + access(all) let collectionData: NFTCollectionData + access(all) let collectionDisplay: MetadataViews.NFTCollectionDisplay init (contractName : String, contractAddress : Address, nftType: Type, collectionData : NFTCollectionData, collectionDisplay : MetadataViews.NFTCollectionDisplay) { self.contractName = contractName @@ -169,13 +169,13 @@ pub contract NFTCatalog { // NFTCatalogProposal // Represents a proposal to the catalog // Includes data about an NFT - pub struct NFTCatalogProposal { - pub let collectionIdentifier : String - pub let metadata : NFTCatalogMetadata - pub let message : String - pub let status : String - pub let proposer : Address - pub let createdTime : UFix64 + access(all) struct NFTCatalogProposal { + access(all) let collectionIdentifier : String + access(all) let metadata : NFTCatalogMetadata + access(all) let message : String + access(all) let status : String + access(all) let proposer : Address + access(all) let createdTime : UFix64 init(collectionIdentifier : String, metadata : NFTCatalogMetadata, message : String, status : String, proposer : Address) { self.collectionIdentifier = collectionIdentifier @@ -192,8 +192,8 @@ pub contract NFTCatalog { If obtaining all elements from the catalog is essential, please use the getCatalogKeys and forEachCatalogKey methods instead. */ - pub fun getCatalog() : {String : NFTCatalogMetadata} { - let snapshot = self.account.borrow<&NFTCatalog.Snapshot>(from: /storage/CatalogSnapshot) + access(all) fun getCatalog() : {String : NFTCatalogMetadata} { + let snapshot = self.account.storage.borrow<&NFTCatalog.Snapshot>(from: /storage/CatalogSnapshot) if snapshot != nil { let snapshot = snapshot! if snapshot.shouldUseSnapshot { @@ -206,23 +206,23 @@ pub contract NFTCatalog { } } - pub fun getCatalogKeys(): [String] { + access(all) fun getCatalogKeys(): [String] { return self.catalog.keys } - pub fun forEachCatalogKey(_ function: ((String): Bool)) { + access(all) fun forEachCatalogKey(_ function: fun (String): Bool) { self.catalog.forEachKey(function) } - pub fun getCatalogEntry(collectionIdentifier : String) : NFTCatalogMetadata? { + access(all) view fun getCatalogEntry(collectionIdentifier : String) : NFTCatalogMetadata? { return self.catalog[collectionIdentifier] } - pub fun getCollectionsForType(nftTypeIdentifier: String) : {String : Bool}? { + access(all) fun getCollectionsForType(nftTypeIdentifier: String) : {String : Bool}? { return self.catalogTypeData[nftTypeIdentifier] } - pub fun getCatalogTypeData() : {String : {String : Bool}} { + access(all) fun getCatalogTypeData() : {String : {String : Bool}} { return self.catalogTypeData } @@ -231,12 +231,10 @@ pub contract NFTCatalog { // @param metadata: The Metadata for the NFT collection that will be stored in the catalog // @param message: A message to the catalog owners // @param proposer: Who is making the proposition(the address needs to be verified) - pub fun proposeNFTMetadata(collectionIdentifier : String, metadata : NFTCatalogMetadata, message : String, proposer : Address) : UInt64 { - let proposerManagerCap = getAccount(proposer).getCapability<&NFTCatalogProposalManager{NFTCatalog.NFTCatalogProposalManagerPublic}>(NFTCatalog.ProposalManagerPublicPath) - - assert(proposerManagerCap.check(), message : "Proposer needs to set up a manager") - - let proposerManagerRef = proposerManagerCap.borrow()! + access(all) fun proposeNFTMetadata(collectionIdentifier : String, metadata : NFTCatalogMetadata, message : String, proposer : Address) : UInt64 { + let proposerManagerRef = getAccount(proposer).capabilities.borrow<&NFTCatalogProposalManager>( + NFTCatalog.ProposalManagerPublicPath + ) ?? panic("Proposer needs to set up a manager") assert(proposerManagerRef.getCurrentProposalEntry()! == collectionIdentifier, message: "Expected proposal entry does not match entry for the proposer") @@ -250,41 +248,39 @@ pub contract NFTCatalog { // Withdraw a proposal from the catalog // @param proposalID: The ID of proposal you want to withdraw - pub fun withdrawNFTProposal(proposalID : UInt64) { + access(all) fun withdrawNFTProposal(proposalID : UInt64) { pre { self.catalogProposals[proposalID] != nil : "Invalid Proposal ID" } let proposal = self.catalogProposals[proposalID]! let proposer = proposal.proposer - let proposerManagerCap = getAccount(proposer).getCapability<&NFTCatalogProposalManager{NFTCatalog.NFTCatalogProposalManagerPublic}>(NFTCatalog.ProposalManagerPublicPath) - - assert(proposerManagerCap.check(), message : "Proposer needs to set up a manager") - - let proposerManagerRef = proposerManagerCap.borrow()! + let proposerManagerRef = getAccount(proposer).capabilities.borrow<&NFTCatalogProposalManager>( + NFTCatalog.ProposalManagerPublicPath + ) ?? panic("Proposer needs to set up a manager") assert(proposerManagerRef.getCurrentProposalEntry()! == proposal.collectionIdentifier, message: "Expected proposal entry does not match entry for the proposer") self.removeCatalogProposal(proposalID : proposalID) } - pub fun getCatalogProposals() : {UInt64 : NFTCatalogProposal} { + access(all) fun getCatalogProposals() : {UInt64 : NFTCatalogProposal} { return self.catalogProposals } - pub fun getCatalogProposalEntry(proposalID : UInt64) : NFTCatalogProposal? { + access(all) view fun getCatalogProposalEntry(proposalID : UInt64) : NFTCatalogProposal? { return self.catalogProposals[proposalID] } - pub fun getCatalogProposalKeys() : [UInt64] { + access(all) fun getCatalogProposalKeys() : [UInt64] { return self.catalogProposals.keys } - pub fun forEachCatalogProposalKey(_ function: ((UInt64): Bool)) { + access(all) fun forEachCatalogProposalKey(_ function: fun (UInt64): Bool) { self.catalogProposals.forEachKey(function) } - pub fun createNFTCatalogProposalManager(): @NFTCatalogProposalManager { + access(all) fun createNFTCatalogProposalManager(): @NFTCatalogProposalManager { return <-create NFTCatalogProposalManager() } diff --git a/contracts/nft-catalog/NFTCatalogAdmin.cdc b/contracts/nft-catalog/NFTCatalogAdmin.cdc index 175453d..8c90fa1 100644 --- a/contracts/nft-catalog/NFTCatalogAdmin.cdc +++ b/contracts/nft-catalog/NFTCatalogAdmin.cdc @@ -6,11 +6,13 @@ import "NFTCatalog" // a proxy resource to receive a capability that lets you make changes to the NFT Catalog // and manage proposals -pub contract NFTCatalogAdmin { +access(all) contract NFTCatalogAdmin { + + access(all) entitlement CatalogActions // AddProposalAccepted // Emitted when a proposal to add a new catalog item has been approved by an admin - pub event AddProposalAccepted( + access(all) event AddProposalAccepted( proposer: Address, collectionIdentifier : String, contractName : String, @@ -20,7 +22,7 @@ pub contract NFTCatalogAdmin { // UpdateProposalAccepted // Emitted when a proposal to update a catalog item has been approved by an admin - pub event UpdateProposalAccepted( + access(all) event UpdateProposalAccepted( proposer: Address, collectionIdentifier : String, contractName : String, @@ -30,7 +32,7 @@ pub contract NFTCatalogAdmin { // ProposalRejected // Emitted when a proposal to add or update a catalog item has been rejected. - pub event ProposalRejected( + access(all) event ProposalRejected( proposer: Address, collectionIdentifier : String, contractName : String, @@ -38,33 +40,33 @@ pub contract NFTCatalogAdmin { displayName : String ) - pub let AdminPrivatePath: PrivatePath - pub let AdminStoragePath: StoragePath + access(all) let AdminPrivatePath: PrivatePath + access(all) let AdminStoragePath: StoragePath - pub let AdminProxyPublicPath: PublicPath - pub let AdminProxyStoragePath: StoragePath + access(all) let AdminProxyPublicPath: PublicPath + access(all) let AdminProxyStoragePath: StoragePath // Admin // Admin resource to manage NFT Catalog - pub resource Admin { + access(all) resource Admin { - pub fun addCatalogEntry(collectionIdentifier: String, metadata : NFTCatalog.NFTCatalogMetadata) { + access(CatalogActions) fun addCatalogEntry(collectionIdentifier: String, metadata : NFTCatalog.NFTCatalogMetadata) { NFTCatalog.addCatalogEntry(collectionIdentifier: collectionIdentifier, metadata : metadata) } - pub fun updateCatalogEntry(collectionIdentifier : String , metadata : NFTCatalog.NFTCatalogMetadata) { + access(CatalogActions) fun updateCatalogEntry(collectionIdentifier : String , metadata : NFTCatalog.NFTCatalogMetadata) { NFTCatalog.updateCatalogEntry(collectionIdentifier: collectionIdentifier, metadata : metadata) } - pub fun removeCatalogEntry(collectionIdentifier : String) { + access(CatalogActions) fun removeCatalogEntry(collectionIdentifier : String) { NFTCatalog.removeCatalogEntry(collectionIdentifier : collectionIdentifier) } - pub fun removeCatalogEntryUnsafe(collectionIdentifier : String, nftTypeIdentifier: String) { + access(CatalogActions) fun removeCatalogEntryUnsafe(collectionIdentifier : String, nftTypeIdentifier: String) { NFTCatalog.removeCatalogEntryUnsafe(collectionIdentifier : collectionIdentifier, nftTypeIdentifier: nftTypeIdentifier) } - pub fun approveCatalogProposal(proposalID : UInt64) { + access(CatalogActions) fun approveCatalogProposal(proposalID : UInt64) { pre { NFTCatalog.getCatalogProposalEntry(proposalID : proposalID) != nil : "Invalid Proposal ID" NFTCatalog.getCatalogProposalEntry(proposalID : proposalID)!.status == "IN_REVIEW" : "Invalid Proposal" @@ -94,7 +96,7 @@ pub contract NFTCatalogAdmin { } } - pub fun rejectCatalogProposal(proposalID : UInt64) { + access(CatalogActions) fun rejectCatalogProposal(proposalID : UInt64) { pre { NFTCatalog.getCatalogProposalEntry(proposalID : proposalID) != nil : "Invalid Proposal ID" NFTCatalog.getCatalogProposalEntry(proposalID : proposalID)!.status == "IN_REVIEW" : "Invalid Proposal" @@ -111,7 +113,7 @@ pub contract NFTCatalogAdmin { ) } - pub fun removeCatalogProposal(proposalID : UInt64) { + access(CatalogActions) fun removeCatalogProposal(proposalID : UInt64) { pre { NFTCatalog.getCatalogProposalEntry(proposalID : proposalID) != nil : "Invalid Proposal ID" } @@ -125,16 +127,16 @@ pub contract NFTCatalogAdmin { // AdminProxy // A proxy resource that can store // a capability to admin controls - pub resource interface IAdminProxy { - pub fun addCapability(capability : Capability<&Admin>) - pub fun hasCapability() : Bool + access(all) resource interface IAdminProxy { + access(all) fun addCapability(capability : Capability) + access(all) fun hasCapability() : Bool } - pub resource AdminProxy : IAdminProxy { + access(all) resource AdminProxy : IAdminProxy { - access(self) var capability : Capability<&Admin>? + access(self) var capability : Capability? - pub fun addCapability(capability : Capability<&Admin>) { + access(all) fun addCapability(capability : Capability) { pre { capability.check() : "Invalid Admin Capability" self.capability == nil : "Admin Proxy already set" @@ -142,11 +144,11 @@ pub contract NFTCatalogAdmin { self.capability = capability } - pub fun getCapability() : Capability<&Admin>? { + access(all) fun getCapability() : Capability? { return self.capability } - pub fun hasCapability() : Bool { + access(all) fun hasCapability() : Bool { return self.capability != nil } @@ -156,7 +158,7 @@ pub contract NFTCatalogAdmin { } - pub fun createAdminProxy() : @AdminProxy { + access(all) fun createAdminProxy() : @AdminProxy { return <- create AdminProxy() } @@ -169,7 +171,6 @@ pub contract NFTCatalogAdmin { let admin <- create Admin() - self.account.save(<-admin, to: self.AdminStoragePath) - self.account.link<&Admin>(self.AdminPrivatePath, target: self.AdminStoragePath) + self.account.storage.save(<-admin, to: self.AdminStoragePath) } } \ No newline at end of file diff --git a/example/contracts/Importer.cdc b/example/contracts/Importer.cdc index 6b8bdd6..e60e858 100644 --- a/example/contracts/Importer.cdc +++ b/example/contracts/Importer.cdc @@ -2,9 +2,7 @@ import "FungibleToken" import "NonFungibleToken" import "MetadataViews" import "ViewResolver" -import "HybridCustody" import "NFTCatalog" -import "NFTCatalogAdmin" import "FlowToken" import "LostAndFoundHelper" import "DapperOffersV2" @@ -12,9 +10,9 @@ import "TopShot" import "AddressUtils" import "ScopedNFTProviders" import "ScopedFTProviders" -import "FindViews" -import "FLOAT" +import "ExampleToken" +import "ExampleNFT" // This contract doesn't do anything, it's just to show that deployments work // with this import system -pub contract Importer { } +access(all) contract Importer { } diff --git a/example/test.flow.json b/example/test.flow.json index 9084e33..c932125 100644 --- a/example/test.flow.json +++ b/example/test.flow.json @@ -1,6 +1,7 @@ { "networks": { "emulator": "127.0.0.1:3569", + "testing": "127.0.0.1:3569", "mainnet": "access.mainnet.nodes.onflow.org:9000", "sandboxnet": "access.sandboxnet.nodes.onflow.org:9000", "testnet": "access.devnet.nodes.onflow.org:9000" diff --git a/flow.json b/flow.json index 9bc9c7b..9b41f24 100644 --- a/flow.json +++ b/flow.json @@ -1,6 +1,7 @@ { "networks": { "emulator": "127.0.0.1:3569", + "testing": "127.0.0.1:3569", "mainnet": "access.mainnet.nodes.onflow.org:9000", "sandboxnet": "access.sandboxnet.nodes.onflow.org:9000", "testnet": "access.devnet.nodes.onflow.org:9000" @@ -28,6 +29,14 @@ "mainnet": "0x1d7e57aa55817448" } }, + "Burner": { + "source": "./contracts/Burner.cdc", + "aliases": { + "emulator": "0xf8d6e0586b0a20c7", + "testnet": "0x9a0766d93b6608b7", + "mainnet": "0xf233dcee88fe0abe" + } + }, "MetadataViews": { "source": "./contracts/MetadataViews.cdc", "aliases": { @@ -52,6 +61,14 @@ "mainnet": "0xf233dcee88fe0abe" } }, + "FungibleTokenSwitchboard": { + "source": "./contracts/FungibleTokenSwitchboard.cdc", + "aliases": { + "emulator": "0xee82856bf20e2aa6", + "testnet": "0x9a0766d93b6608b7", + "mainnet": "0xf233dcee88fe0abe" + } + }, "ViewResolver": { "source": "./contracts/ViewResolver.cdc", "aliases": { @@ -132,8 +149,8 @@ "mainnet": "0xd8a7e05a7ac670c0" } }, - "NFTProviderAndCollectionPublicFactory": { - "source": "./contracts/hybrid-custody/factories/NFTProviderAndCollectionPublicFactory.cdc", + "NFTProviderAndCollectionFactory": { + "source": "./contracts/hybrid-custody/factories/NFTProviderAndCollectionFactory.cdc", "aliases": { "emulator": "0xf8d6e0586b0a20c7", "testnet": "0x294e44e1ec6993c6", @@ -177,7 +194,7 @@ "aliases": { "emulator": "0xf8d6e0586b0a20c7", "testnet": "0x8c5303eaa26202d6", - "mainnet": "0xf919ee77447b7497" + "mainnet": "0xe467b9dd11fa00df" } }, "TokenForwarding": { @@ -323,11 +340,180 @@ "mainnet": "0x2d4c3caffbeab845", "testnet": "0x4d47bf3ce5e4393f" } + }, + "ExampleNFT": { + "source": "./contracts/example/ExampleNFT.cdc", + "aliases": { + "emulator": "0xf8d6e0586b0a20c7", + "testing": "0x0000000000000007" + } + }, + "ExampleToken": { + "source": "./contracts/example/ExampleToken.cdc", + "aliases": { + "emulator": "0xf8d6e0586b0a20c7", + "testing": "0x0000000000000007" + } + }, + "ContractManager": { + "source": "./contracts/flowty-drops/ContractManager.cdc", + "aliases": { + "testing": "0x0000000000000006", + "testnet": "0x772a10c786851a1b", + "emulator": "0xf8d6e0586b0a20c7" + } + }, + "ContractInitializer": { + "source": "./contracts/flowty-drops/initializers/ContractInitializer.cdc", + "aliases": { + "testing": "0x0000000000000006", + "testnet": "0x772a10c786851a1b", + "emulator": "0xf8d6e0586b0a20c7" + } + }, + "ContractBorrower": { + "source": "./contracts/flowty-drops/initializers/ContractBorrower.cdc", + "aliases": { + "testing": "0x0000000000000006", + "testnet": "0x772a10c786851a1b", + "emulator": "0xf8d6e0586b0a20c7" + } + }, + "OpenEditionInitializer": { + "source": "./contracts/flowty-drops/initializers/OpenEditionInitializer.cdc", + "aliases": { + "testing": "0x0000000000000006", + "testnet": "0x772a10c786851a1b", + "emulator": "0xf8d6e0586b0a20c7" + } + }, + "BaseCollection": { + "source": "./contracts/flowty-drops/nft/BaseCollection.cdc", + "aliases": { + "testing": "0x0000000000000006", + "testnet": "0x772a10c786851a1b", + "emulator": "0xf8d6e0586b0a20c7" + } + }, + "ContractFactoryTemplate": { + "source": "./contracts/flowty-drops/nft/ContractFactoryTemplate.cdc", + "aliases": { + "testing": "0x0000000000000006", + "testnet": "0x772a10c786851a1b", + "emulator": "0xf8d6e0586b0a20c7" + } + }, + "ContractFactory": { + "source": "./contracts/flowty-drops/nft/ContractFactory.cdc", + "aliases": { + "testing": "0x0000000000000006", + "testnet": "0x772a10c786851a1b", + "emulator": "0xf8d6e0586b0a20c7" + } + }, + "OpenEditionTemplate": { + "source": "./contracts/flowty-drops/nft/OpenEditionTemplate.cdc", + "aliases": { + "testing": "0x0000000000000006", + "testnet": "0x772a10c786851a1b", + "emulator": "0xf8d6e0586b0a20c7" + } + }, + "UniversalCollection": { + "source": "./contracts/flowty-drops/nft/UniversalCollection.cdc", + "aliases": { + "testing": "0x0000000000000006", + "testnet": "0x772a10c786851a1b", + "emulator": "0xf8d6e0586b0a20c7" + } + }, + "BaseNFT": { + "source": "./contracts/flowty-drops/nft/BaseNFT.cdc", + "aliases": { + "testing": "0x0000000000000006", + "testnet": "0x772a10c786851a1b", + "emulator": "0xf8d6e0586b0a20c7" + } + }, + "NFTMetadata": { + "source": "./contracts/flowty-drops/nft/NFTMetadata.cdc", + "aliases": { + "testing": "0x0000000000000006", + "testnet": "0x772a10c786851a1b", + "emulator": "0xf8d6e0586b0a20c7" + } + }, + "FlowtyDrops": { + "source": "./contracts/flowty-drops/FlowtyDrops.cdc", + "aliases": { + "testing": "0x0000000000000006", + "testnet": "0x772a10c786851a1b", + "emulator": "0xf8d6e0586b0a20c7" + } + }, + "DropFactory": { + "source": "./contracts/flowty-drops/DropFactory.cdc", + "aliases": { + "testing": "0x0000000000000006", + "testnet": "0x772a10c786851a1b", + "emulator": "0xf8d6e0586b0a20c7" + } + }, + "FlowtyActiveCheckers": { + "source": "./contracts/flowty-drops/FlowtyActiveCheckers.cdc", + "aliases": { + "testing": "0x0000000000000006", + "testnet": "0x772a10c786851a1b", + "emulator": "0xf8d6e0586b0a20c7" + } + }, + "FlowtyPricers": { + "source": "./contracts/flowty-drops/FlowtyPricers.cdc", + "aliases": { + "testing": "0x0000000000000006", + "testnet": "0x772a10c786851a1b", + "emulator": "0xf8d6e0586b0a20c7" + } + }, + "FlowtyAddressVerifiers": { + "source": "./contracts/flowty-drops/FlowtyAddressVerifiers.cdc", + "aliases": { + "testing": "0x0000000000000006", + "testnet": "0x772a10c786851a1b", + "emulator": "0xf8d6e0586b0a20c7" + } + }, + "DropTypes": { + "source": "./contracts/flowty-drops/DropTypes.cdc", + "aliases": { + "testing": "0x0000000000000006", + "testnet": "0x934da91a977f1ac4", + "emulator": "0xf8d6e0586b0a20c7" + } + }, + "CapabilityCache": { + "source": "./contracts/capability-cache/CapabilityCache.cdc", + "aliases": { + "testing": "0x0000000000000007", + "emulator": "0xf8d6e0586b0a20c7", + "testnet": "0x83d75469f66d2ee6", + "mainnet": "0xacc5081c003e24cf" + } + }, + "FungibleTokenRouter": { + "source": "./contracts/fungible-token-router/FungibleTokenRouter.cdc", + "aliases": { + "testing": "0x0000000000000007", + "emulator": "0xf8d6e0586b0a20c7", + "testnet": "0x83231f90a288bc35", + "mainnet": "0x707c0b39a8d689cb" + } } }, "deployments": { "emulator": { "emulator-account": [ + "Burner", "NonFungibleToken", "MetadataViews", "ViewResolver", @@ -351,11 +537,25 @@ "ScopedNFTProviders", "ScopedFTProviders", "FindViews", - "FLOAT" + "FLOAT", + "ExampleNFT", + "ExampleToken", + "HybridCustody", + "CapabilityFactory", + "CapabilityDelegator", + "CapabilityFilter", + "FTAllFactory", + "FTBalanceFactory", + "FTProviderFactory", + "FTReceiverFactory", + "NFTCollectionPublicFactory", + "NFTProviderAndCollectionFactory", + "NFTProviderFactory" ], "emulator-ft": [ "FungibleToken", - "FungibleTokenMetadataViews" + "FungibleTokenMetadataViews", + "FungibleTokenSwitchboard" ], "emulator-flowtoken": [ "FlowToken" diff --git a/package-lock.json b/package-lock.json index e652797..36f0d99 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@flowtyio/flow-contracts", - "version": "0.0.8", + "version": "0.1.0-beta.24", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@flowtyio/flow-contracts", - "version": "0.0.8", + "version": "0.1.0-beta.24", "license": "MIT", "dependencies": { "commander": "^11.0.0" diff --git a/package.json b/package.json index 1dc950b..68a3174 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@flowtyio/flow-contracts", - "version": "0.0.18", + "version": "0.1.0-beta.32", "main": "index.json", "description": "An NPM package for common flow contracts", "author": "flowtyio", diff --git a/test.sh b/test.sh index 0ffdc48..eb6e0f1 100755 --- a/test.sh +++ b/test.sh @@ -16,18 +16,18 @@ npx flow-contracts add-all --config "$configPath" echo "starting the flow emulator in 5 seconds..." sleep 5 -nohup flow emulator & +nohup flow-c1 emulator & sleep 5 echo "deploying contracts..." -flow project deploy --update +flow-c1 project deploy --update echo "deployment complete!" sleep 3 echo "cleaning up..." -pkill -f flow +pkill -f flow-c1 rm flow.json exit 0