From 5fd8b937315b5286d105cf29041e862892e8de43 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Fri, 8 Dec 2023 17:26:46 -0600 Subject: [PATCH 1/5] update .gitignore --- .gitignore | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 9345682..7d3f745 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ -lib/js/test/node_modules/* \ No newline at end of file +lib/js/test/node_modules/* +.idea +*.pkey +*.pub +.env \ No newline at end of file From 3dccb2754d2de9f47ee9fb276e02003158c4a1eb Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Fri, 8 Dec 2023 17:27:06 -0600 Subject: [PATCH 2/5] add FlowToken contract & transfer txn --- contracts/utility/FlowToken.cdc | 274 ++++++++++++++++++++++ transactions/flow-token/transfer_flow.cdc | 23 ++ 2 files changed, 297 insertions(+) create mode 100644 contracts/utility/FlowToken.cdc create mode 100644 transactions/flow-token/transfer_flow.cdc diff --git a/contracts/utility/FlowToken.cdc b/contracts/utility/FlowToken.cdc new file mode 100644 index 0000000..35bd96b --- /dev/null +++ b/contracts/utility/FlowToken.cdc @@ -0,0 +1,274 @@ +import "FungibleToken" +import "MetadataViews" +import "FungibleTokenMetadataViews" +import "ViewResolver" + +pub contract FlowToken: FungibleToken, ViewResolver { + + // 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) + + // Event that is emitted when tokens are withdrawn from a Vault + pub event TokensWithdrawn(amount: UFix64, from: Address?) + + // Event that is emitted when tokens are deposited to a Vault + pub 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) + + // Event that is emitted when a new minter resource is created + pub event MinterCreated(allowedAmount: UFix64) + + // Event that is emitted when a new burner resource is created + pub 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. + // + pub resource Vault: FungibleToken.Provider, FungibleToken.Receiver, FungibleToken.Balance, MetadataViews.Resolver { + + // holds the balance of a users tokens + pub var balance: UFix64 + + // initialize the balance at resource creation time + init(balance: UFix64) { + self.balance = balance + } + + // withdraw + // + // Function that takes an integer 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. + // + pub fun withdraw(amount: UFix64): @FungibleToken.Vault { + self.balance = self.balance - amount + emit TokensWithdrawn(amount: amount, from: self.owner?.address) + return <-create Vault(balance: amount) + } + + // 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. + pub 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) + vault.balance = 0.0 + 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() + } + + /// Get a Metadata View from FlowToken + /// + /// @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) + } + } + + // 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. + // + pub fun createEmptyVault(): @FungibleToken.Vault { + return <-create Vault(balance: 0.0) + } + + pub fun getViews(): [Type] { + return [Type(), + Type(), + Type()] + } + + /// Get a Metadata View from FlowToken + /// + /// @param view: The Type of the desired view. + /// @return A structure representing the requested view. + /// + pub fun resolveView(_ view: Type): AnyStruct? { + switch view { + case Type(): + return FungibleTokenMetadataViews.FTView( + ftDisplay: self.resolveView(Type()) as! FungibleTokenMetadataViews.FTDisplay?, + ftVaultData: self.resolveView(Type()) as! FungibleTokenMetadataViews.FTVaultData? + ) + case Type(): + let media = MetadataViews.Media( + file: MetadataViews.HTTPFile( + url: "https://assets.website-files.com/5f6294c0c7a8cdd643b1c820/5f6294c0c7a8cda55cb1c936_Flow_Wordmark.svg" + ), + mediaType: "image/svg+xml" + ) + let medias = MetadataViews.Medias([media]) + 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", + externalURL: MetadataViews.ExternalURL("https://flow.com"), + logos: medias, + socials: { + "twitter": MetadataViews.ExternalURL("https://twitter.com/flow_blockchain") + } + ) + case Type(): + 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() + }) + ) + } + return nil + } + + pub resource Administrator { + // createNewMinter + // + // Function that creates and returns a new minter resource + // + pub 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 { + + // the amount of tokens that the minter is allowed to mint + pub 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 { + pre { + amount > UFix64(0): "Amount minted must be greater than zero" + amount <= self.allowedAmount: "Amount minted must be less than the allowed amount" + } + FlowToken.totalSupply = FlowToken.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. + // + 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) + } + } + + init(adminAccount: AuthAccount) { + 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) + + // 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 + ) + + // 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 admin <- create Administrator() + adminAccount.save(<-admin, to: /storage/flowTokenAdmin) + + // Emit an event that shows that the contract was initialized + emit TokensInitialized(initialSupply: self.totalSupply) + } +} \ No newline at end of file diff --git a/transactions/flow-token/transfer_flow.cdc b/transactions/flow-token/transfer_flow.cdc new file mode 100644 index 0000000..2c911bb --- /dev/null +++ b/transactions/flow-token/transfer_flow.cdc @@ -0,0 +1,23 @@ +import "FungibleToken" +import "FlowToken" + +transaction(recipient: Address, amount: UFix64) { + + let providerVault: &FlowToken.Vault + let receiver: &{FungibleToken.Receiver} + + prepare(signer: AuthAccount) { + self.providerVault = signer.borrow<&FlowToken.Vault>(from: /storage/flowTokenVault)! + self.receiver = getAccount(recipient).getCapability<&{FungibleToken.Receiver}>(/public/flowTokenReceiver) + .borrow() + ?? panic("Could not borrow receiver reference") + } + + execute { + self.receiver.deposit( + from: <-self.providerVault.withdraw( + amount: amount + ) + ) + } +} From 41c8542a28395e75a3c4ec0e6d11fe1b1a3a9786 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Fri, 8 Dec 2023 17:27:33 -0600 Subject: [PATCH 3/5] add HybridCustody contracts & update flow.json --- .../hybrid-custody/CapabilityDelegator.cdc | 174 +++ .../hybrid-custody/CapabilityFactory.cdc | 105 ++ contracts/hybrid-custody/CapabilityFilter.cdc | 215 +++ contracts/hybrid-custody/HybridCustody.cdc | 1223 +++++++++++++++++ .../hybrid-custody/factories/FTAllFactory.cdc | 10 + .../factories/FTBalanceFactory.cdc | 10 + .../factories/FTProviderFactory.cdc | 10 + .../factories/FTReceiverBalanceFactory.cdc | 10 + .../factories/FTReceiverFactory.cdc | 10 + .../factories/NFTCollectionPublicFactory.cdc | 10 + .../NFTProviderAndCollectionFactory.cdc | 10 + .../factories/NFTProviderFactory.cdc | 10 + flow.json | 196 ++- 13 files changed, 1966 insertions(+), 27 deletions(-) create mode 100644 contracts/hybrid-custody/CapabilityDelegator.cdc create mode 100644 contracts/hybrid-custody/CapabilityFactory.cdc create mode 100644 contracts/hybrid-custody/CapabilityFilter.cdc create mode 100644 contracts/hybrid-custody/HybridCustody.cdc create mode 100644 contracts/hybrid-custody/factories/FTAllFactory.cdc create mode 100644 contracts/hybrid-custody/factories/FTBalanceFactory.cdc create mode 100644 contracts/hybrid-custody/factories/FTProviderFactory.cdc create mode 100644 contracts/hybrid-custody/factories/FTReceiverBalanceFactory.cdc create mode 100644 contracts/hybrid-custody/factories/FTReceiverFactory.cdc create mode 100644 contracts/hybrid-custody/factories/NFTCollectionPublicFactory.cdc create mode 100644 contracts/hybrid-custody/factories/NFTProviderAndCollectionFactory.cdc create mode 100644 contracts/hybrid-custody/factories/NFTProviderFactory.cdc diff --git a/contracts/hybrid-custody/CapabilityDelegator.cdc b/contracts/hybrid-custody/CapabilityDelegator.cdc new file mode 100644 index 0000000..f342aaf --- /dev/null +++ b/contracts/hybrid-custody/CapabilityDelegator.cdc @@ -0,0 +1,174 @@ +/// CapabilityDelegator is a contract used to share Capabiltities to other accounts. It is used by the +/// HybridCustody contract to allow more flexible sharing of Capabilities when an app wants to share things +/// that aren't the NFT-standard interface types. +/// +/// Inside of CapabilityDelegator is a resource called `Delegator` which maintains a mapping of public and private +/// Capabilities. They cannot and should not be mixed. A public `Delegator` is able to be borrowed by anyone, whereas a +/// private `Delegator` can only be borrowed from the child account when you have access to the full `ChildAccount` +/// resource. +/// +pub contract CapabilityDelegator { + + /* --- Canonical Paths --- */ + // + pub let StoragePath: StoragePath + pub let PrivatePath: PrivatePath + pub let PublicPath: PublicPath + + /* --- Events --- */ + // + pub event DelegatorCreated(id: UInt64) + pub 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? { + post { + result == nil || type.isSubtype(of: result.getType()): "incorrect returned capability type" + } + } + pub fun findFirstPrivateType(_ type: Type): Type? + pub fun getAllPrivate(): [Capability] + } + + /// Exposes public Capability retrieval + /// + pub resource interface GetterPublic { + pub fun getPublicCapability(_ type: Type): Capability? { + post { + result == nil || type.isSubtype(of: result.getType()): "incorrect returned capability type " + } + } + + pub fun findFirstPublicType(_ type: Type): Type? + pub 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(self) let privateCapabilities: {Type: Capability} + access(self) let publicCapabilities: {Type: Capability} + + // ------ Begin Getter methods + // + /// Returns the public Capability of the given Type if it exists + /// + pub fun getPublicCapability(_ type: Type): Capability? { + return self.publicCapabilities[type] + } + + /// Returns the private Capability of the given Type if it exists + /// + /// + /// @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? { + return self.privateCapabilities[type] + } + + /// Returns all public Capabilities + /// + /// @return List of all public Capabilities + /// + pub fun getAllPublic(): [Capability] { + return self.publicCapabilities.values + } + + /// Returns all private Capabilities + /// + /// @return List of all private Capabilities + /// + pub fun getAllPrivate(): [Capability] { + return self.privateCapabilities.values + } + + /// Returns the first public Type that is a subtype of the given Type + /// + /// @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? { + for t in self.publicCapabilities.keys { + if t.isSubtype(of: type) { + return t + } + } + + return nil + } + + /// Returns the first private Type that is a subtype of the given Type + /// + /// @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? { + for t in self.privateCapabilities.keys { + if t.isSubtype(of: type) { + return t + } + } + + return nil + } + // ------- End Getter methods + + /// Adds a Capability to the Delegator + /// + /// @param cap: Capability to add + /// @param isPublic: Whether the Capability should be public or private + /// + pub fun addCapability(cap: Capability, isPublic: Bool) { + pre { + cap.check<&AnyResource>(): "Invalid Capability provided" + } + if isPublic { + self.publicCapabilities.insert(key: cap.getType(), cap) + } else { + self.privateCapabilities.insert(key: cap.getType(), cap) + } + emit DelegatorUpdated(id: self.uuid, capabilityType: cap.getType(), isPublic: isPublic, active: true) + } + + /// Removes a Capability from the Delegator + /// + /// @param cap: Capability to remove + /// + pub 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) + } + + if let removedPrivate = self.privateCapabilities.remove(key: cap.getType()) { + emit DelegatorUpdated(id: self.uuid, capabilityType: cap.getType(), isPublic: false, active: false) + } + } + + init() { + self.privateCapabilities = {} + self.publicCapabilities = {} + } + } + + /// Creates a new Delegator and returns it + /// + /// @return Newly created Delegator + /// + pub fun createDelegator(): @Delegator { + let delegator <- create Delegator() + emit DelegatorCreated(id: delegator.uuid) + return <- delegator + } + + init() { + let identifier = "CapabilityDelegator_".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/CapabilityFactory.cdc b/contracts/hybrid-custody/CapabilityFactory.cdc new file mode 100644 index 0000000..ee777f4 --- /dev/null +++ b/contracts/hybrid-custody/CapabilityFactory.cdc @@ -0,0 +1,105 @@ +/// # Capability Factory +/// +/// This contract defines a Factory interface and a Manager resource to contain Factory implementations, as well as a +/// Getter interface for retrieval of contained Factories. +/// +/// A Factory is defines a method getCapability() which defines the retrieval pattern of a Capability from a given +/// account at the specified path. This pattern arose out of a need to retrieve arbitrary & castable Capabilities from +/// an account under the static typing constraints inherent to Cadence. +/// +/// The Manager resource is a container for Factories, and implements the Getter interface. +/// +/// **Note:** It's generally an anti-pattern to pass around AuthAccount references; however, the need for castable +/// 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 { + + pub let StoragePath: StoragePath + pub let PrivatePath: PrivatePath + pub let PublicPath: PublicPath + + /// 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 + } + + /// 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}? + } + + /// Manager is a resource that contains Factories and implements the Getter interface for retrieval of contained + /// Factories + /// + pub resource Manager: Getter { + /// Mapping of Factories indexed on Type of Capability they retrieve + pub 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] { + return self.factories.keys + } + + /// Retrieves a Factory from the Manager, returning it or nil if it doesn't exist + /// + /// @param t: Type the Factory is indexed on + /// + pub fun getFactory(_ t: Type): {CapabilityFactory.Factory}? { + return self.factories[t] + } + + /// Adds a Factory to the Manager, conditioned on the Factory not already existing + /// + /// @param t: Type of Capability the Factory retrieves + /// @param f: Factory to add + /// + pub fun addFactory(_ t: Type, _ f: {CapabilityFactory.Factory}) { + pre { + !self.factories.containsKey(t): "Factory of given type already exists" + } + self.factories[t] = f + } + + /// Updates a Factory in the Manager, adding if it didn't already exist + /// + /// @param t: Type of Capability the Factory retrieves + /// @param f: Factory to replace existing Factory + /// + pub fun updateFactory(_ t: Type, _ f: {CapabilityFactory.Factory}) { + self.factories[t] = f + } + + /// Removes a Factory from the Manager, returning it or nil if it didn't exist + /// + /// @param t: Type the Factory is indexed on + /// + pub fun removeFactory(_ t: Type): {CapabilityFactory.Factory}? { + return self.factories.remove(key: t) + } + + init () { + self.factories = {} + } + } + + /// Creates a Manager resource + /// + /// @return Manager resource + pub 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 new file mode 100644 index 0000000..7a76509 --- /dev/null +++ b/contracts/hybrid-custody/CapabilityFilter.cdc @@ -0,0 +1,215 @@ +/// CapabilityFilter defines `Filter`, an interface to sit on top of a ChildAccount's capabilities. Requested +/// capabilities will only return if the filter's `allowed` method returns true. +/// +/// Along with the `Filter` interface are three implementations: +/// - `DenylistFilter` - A filter which contains a mapping of denied Types +/// - `AllowlistFilter` - A filter which contains a mapping of allowed Types +/// - `AllowAllFilter` - A passthrough, all requested capabilities are allowed +/// +pub contract CapabilityFilter { + + /* --- Canonical Paths --- */ + // + pub let StoragePath: StoragePath + pub let PublicPath: PublicPath + pub let PrivatePath: PrivatePath + + /* --- Events --- */ + // + pub 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 + } + + /// `DenylistFilter` is a `Filter` which contains a mapping of denied Types + /// + pub 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 + /// types + access(self) let deniedTypes: {Type: Bool} + + /// Adds a type to the mapping of denied types with a value of true + /// + /// @param type: The type to add to the denied types mapping + /// + pub fun addType(_ type: Type) { + self.deniedTypes.insert(key: type, true) + emit FilterUpdated(id: self.uuid, filterType: self.getType(), type: type, active: true) + } + + /// Removes a type from the mapping of denied types + /// + /// @param type: The type to remove from the denied types mapping + /// + pub 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 + /// + pub 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 { + if let item = cap.borrow<&AnyResource>() { + return !self.deniedTypes.containsKey(item.getType()) + } + + return false + } + + /// Returns details about this filter + /// + /// @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 { + return { + "type": self.getType(), + "deniedTypes": self.deniedTypes.keys + } + } + + init() { + self.deniedTypes = {} + } + } + + /// `AllowlistFilter` is a `Filter` which contains a mapping of allowed Types + /// + pub resource AllowlistFilter: Filter { + // allowedTypes + // Represents the set of underlying types which are allowed to be + // returned by a RestrictedChildAccount. The filter will borrow + // a requested capability, and make sure that the type it gets back is + // in the list of allowed types + access(self) let allowedTypes: {Type: Bool} + + /// Adds a type to the mapping of allowed types with a value of true + /// + /// @param type: The type to add to the allowed types mapping + /// + pub fun addType(_ type: Type) { + self.allowedTypes.insert(key: type, true) + emit FilterUpdated(id: self.uuid, filterType: self.getType(), type: type, active: true) + } + + /// Removes a type from the mapping of allowed types + /// + /// @param type: The type to remove from the denied types mapping + /// + pub 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 + /// + pub 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 { + if let item = cap.borrow<&AnyResource>() { + return self.allowedTypes.containsKey(item.getType()) + } + + return false + } + + /// Returns details about this filter + /// + /// @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 { + return { + "type": self.getType(), + "allowedTypes": self.allowedTypes.keys + } + } + + init() { + self.allowedTypes = {} + } + } + + /// AllowAllFilter is a passthrough, all requested capabilities are allowed + /// + pub 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 { + return true + } + + /// Returns details about this filter + /// + /// @return A struct containing details about this filter including this Filter's Type indexed on the `type` + /// key + /// + pub fun getDetails(): AnyStruct { + return { + "type": self.getType() + } + } + } + + /// Creates a new `Filter` of the given type + /// + /// @param t: The type of `Filter` to create + /// @return: A new instance of the given `Filter` type + /// + pub fun create(_ t: Type): @AnyResource{Filter} { + post { + result.getType() == t + } + + switch t { + case Type<@AllowAllFilter>(): + return <- create AllowAllFilter() + case Type<@AllowlistFilter>(): + return <- create AllowlistFilter() + case Type<@DenylistFilter>(): + return <- create DenylistFilter() + } + + panic("unsupported type requested: ".concat(t.identifier)) + } + + init() { + let identifier = "CapabilityFilter_".concat(self.account.address.toString()) + + self.StoragePath = StoragePath(identifier: identifier)! + self.PublicPath = PublicPath(identifier: identifier)! + self.PrivatePath = PrivatePath(identifier: identifier)! + } +} diff --git a/contracts/hybrid-custody/HybridCustody.cdc b/contracts/hybrid-custody/HybridCustody.cdc new file mode 100644 index 0000000..1c5795d --- /dev/null +++ b/contracts/hybrid-custody/HybridCustody.cdc @@ -0,0 +1,1223 @@ +// Third-party imports +import "MetadataViews" + +// HC-owned imports +import "CapabilityFactory" +import "CapabilityDelegator" +import "CapabilityFilter" + +/// HybridCustody defines a framework for sharing accounts via account linking. +/// In the contract, there are three main resources: +/// +/// 1. OwnedAccount - A resource which maintains an AuthAccount Capability, and handles publishing and revoking access +/// of that account via another resource called a ChildAccount +/// 2. ChildAccount - A second resource which exists on the same account as the OwnedAccount and contains the filters +/// and retrieval patterns governing the scope of parent account access. A Capability on this resource is shared to +/// the parent account, enabling Hybrid Custody access to the underlying account. +/// 3. Manager - A resource setup by the parent which manages all child accounts shared with it. The Manager resource +/// also maintains a set of accounts that it "owns", meaning it has a capability to the full OwnedAccount resource +/// and would then also be able to manage the child account's links as it sees fit. +/// +/// Contributors (please add to this list if you contribute!): +/// - Austin Kline - https://twitter.com/austin_flowty +/// - Deniz Edincik - https://twitter.com/bluesign +/// - Giovanni Sanchez - https://twitter.com/gio_incognito +/// - Ashley Daffin - https://twitter.com/web3ashlee +/// - Felipe Ribeiro - https://twitter.com/Frlabs33 +/// +/// Repo reference: https://github.com/onflow/hybrid-custody +/// +pub contract HybridCustody { + + /* --- 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 + + pub let ManagerStoragePath: StoragePath + pub let ManagerPublicPath: PublicPath + pub let ManagerPrivatePath: PrivatePath + + pub let LinkedAccountPrivatePath: PrivatePath + pub let BorrowableAccountPrivatePath: PrivatePath + + /* --- Events --- */ + // + /// Manager creation event + pub event CreatedManager(id: UInt64) + /// OwnedAccount creation event + pub 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) + /// 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) + /// ChildAccount ready to be redeemed by emitted pendingParent + pub event ChildAccountPublished( + ownedAcctID: UInt64, + childAcctID: UInt64, + capDelegatorID: UInt64, + factoryID: UInt64, + filterID: UInt64, + filterType: Type, + child: Address, + 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) + /// Account has been sealed - keys revoked, new AuthAccount Capability generated + pub 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 + } + + /// Public methods anyone can call on an OwnedAccount + /// + pub resource interface OwnedAccountPublic { + /// Returns the addresses of all parent accounts + pub fun getParentAddresses(): [Address] + + /// Returns associated parent addresses and their redeemed status - true if redeemed, false if pending + pub 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? + + /// A callback function to mark a parent as redeemed on the child account. + access(contract) fun setRedeemed(_ addr: Address) + } + + /// Private interface accessible to the owner of the OwnedAccount + /// + pub 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 + + /// 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( + parentAddress: Address, + factory: Capability<&CapabilityFactory.Manager{CapabilityFactory.Getter}>, + filter: Capability<&{CapabilityFilter.Filter}> + ) { + pre { + factory.check(): "Invalid CapabilityFactory.Getter Capability provided" + filter.check(): "Invalid CapabilityFilter Capability provided" + } + } + + /// 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) + + /// 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 + /// parent accounts can still operate normally. + /// Unless this method is executed via the giveOwnership function, this will leave an account **without** an + /// owner. + /// USE WITH EXTREME CAUTION. + pub 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}>) { + 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}>) { + pre { + cap.check(): "Invalid CapabilityFilter Capability provided" + } + } + + /// 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) { + 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) + + /// Returns the address of this OwnedAccount + pub fun getAddress(): Address + + /// Checks if this OwnedAccount is a child of the specified address + pub fun isChildOf(_ addr: Address): Bool + + /// Returns all addresses which are parents of this OwnedAccount + pub fun getParentAddresses(): [Address] + + /// Borrows this OwnedAccount's AuthAccount Capability + pub fun borrowAccount(): &AuthAccount? + + /// Returns the current owner of this account, if there is one + pub fun getOwner(): Address? + + /// Returns the pending owner of this account, if there is one + pub 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() + + /// Revokes all keys on this account + pub 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 + pub fun getCapabilityFactoryManager(): &{CapabilityFactory.Getter}? + pub fun getCapabilityFilter(): &{CapabilityFilter.Filter}? + } + + /// Methods accessible to the designated parent of a ChildAccount + /// + pub resource interface AccountPrivate { + pub fun getCapability(path: CapabilityPath, 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? { + post { + result == nil || [true, nil].contains(self.getManagerCapabilityFilter()?.allowed(cap: result!)): + "Capability is not allowed by this account's Parent" + } + } + access(contract) fun redeemedCallback(_ addr: Address) + access(contract) fun setManagerCapabilityFilter(_ managerCapabilityFilter: Capability<&{CapabilityFilter.Filter}>?) { + pre { + managerCapabilityFilter == nil || managerCapabilityFilter!.check(): "Invalid Manager Capability Filter" + } + } + access(contract) fun parentRemoveChildCallback(parent: Address) + } + + /// 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) { + pre { + cap == nil || cap!.check(): "Invalid Manager Capability Filter" + } + } + } + + /// 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(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 { + + /// Mapping of restricted access child account Capabilities indexed by their address + pub let childAccounts: {Address: Capability<&{AccountPrivate, AccountPublic, MetadataViews.Resolver}>} + /// Mapping of unrestricted owned account Capabilities indexed by their address + pub let ownedAccounts: {Address: Capability<&{OwnedAccountPrivate, OwnedAccountPublic, MetadataViews.Resolver}>} + + /// A bucket of structs so that the Manager resource can be easily extended with new functionality. + pub 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} + + /// 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}>? + + // display metadata for a child account exists on its parent + pub 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?) { + pre { + self.childAccounts[address] != nil: "There is no child account with this address" + } + + if d == nil { + self.childAccountDisplays.remove(key: address) + return + } + + self.childAccountDisplays[address] = d + } + + /// 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}>) { + pre { + self.childAccounts[cap.address] == nil: "There is already a child account with this address" + } + + let acct = cap.borrow() + ?? panic("child account capability could not be borrowed") + + self.childAccounts[cap.address] = cap + + emit AccountUpdated(id: acct.uuid, child: cap.address, parent: self.owner!.address, active: true) + + acct.redeemedCallback(self.owner!.address) + acct.setManagerCapabilityFilter(self.filter) + } + + /// Sets the default Filter Capability for this Manager. Does not propagate to child accounts. + /// + pub 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) { + let acct = self.borrowAccount(addr: childAddress) + ?? panic("child account not found") + + acct.setManagerCapabilityFilter(cap) + } + + /// 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) { + let cap = self.childAccounts.remove(key: addr) + ?? panic("child account not found") + + self.childAccountDisplays.remove(key: addr) + + if !cap.check() { + // Emit event if invalid capability + emit AccountUpdated(id: nil, child: cap.address, parent: self.owner!.address, active: false) + return + } + + let acct = cap.borrow()! + // Get the child account id before removing capability + let id: UInt64 = acct.uuid + + acct.parentRemoveChildCallback(parent: self.owner!.address) + + 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 + /// account initiates unlinking parent from child + /// + access(contract) fun removeParentCallback(child: Address) { + self.childAccounts.remove(key: child) + self.childAccountDisplays.remove(key: child) + } + + /// 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}>) { + pre { + self.ownedAccounts[cap.address] == nil: "There is already an owned account with this address" + } + + let acct = cap.borrow() + ?? panic("owned account capability could not be borrowed") + + // for safety, rotate the auth account capability to prevent any outstanding capabilities from the previous owner + // and revoke all outstanding keys. + acct.rotateAuthAccount() + acct.revokeAllKeys() + + self.ownedAccounts[cap.address] = cap + + emit OwnershipUpdated(id: acct.uuid, child: cap.address, previousOwner: acct.getOwner(), owner: self.owner!.address, active: true) + acct.setOwnerCallback(self.owner!.address) + } + + /// Returns a reference to a child account + /// + pub fun borrowAccount(addr: Address): &{AccountPrivate, AccountPublic, MetadataViews.Resolver}? { + let cap = self.childAccounts[addr] + if cap == nil { + return nil + } + + return cap!.borrow() + } + + /// Returns a reference to a child account's public AccountPublic interface + /// + pub fun borrowAccountPublic(addr: Address): &{AccountPublic, MetadataViews.Resolver}? { + let cap = self.childAccounts[addr] + if cap == nil { + return nil + } + + return cap!.borrow() + } + + /// Returns a reference to an owned account + /// + pub fun borrowOwnedAccount(addr: Address): &{OwnedAccountPrivate, OwnedAccountPublic, MetadataViews.Resolver}? { + if let cap = self.ownedAccounts[addr] { + return cap.borrow() + } + + return nil + } + + /// 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) { + if let acct = self.ownedAccounts.remove(key: addr) { + if acct.check() { + acct.borrow()!.seal() + } + let id: UInt64? = acct.borrow()?.uuid ?? nil + + emit OwnershipUpdated(id: id!, child: addr, previousOwner: self.owner!.address, owner: nil, active: false) + } + // Don't emit an event if nothing was removed + } + + /// Removes the owned Capabilty on the specified account, relinquishing access to the account and publishes a + /// Capability for the specified account. See `OwnedAccount.giveOwnership()` for more details on this method. + /// + /// **NOTE:** The existence of this method does not imply that it is the only way to receive access to a + /// OwnedAccount Capability or that only the labeled `to` account has said access. Rather, this is a convenient + /// 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) { + let acct = self.ownedAccounts.remove(key: addr) + ?? panic("account not found") + + acct.borrow()!.giveOwnership(to: to) + } + + /// Returns an array of child account addresses + /// + pub fun getChildAddresses(): [Address] { + return self.childAccounts.keys + } + + /// Returns an array of owned account addresses + /// + pub fun getOwnedAddresses(): [Address] { + return self.ownedAccounts.keys + } + + /// Retrieves the parent-defined display for the given child account + /// + pub fun getChildAccountDisplay(address: Address): MetadataViews.Display? { + return self.childAccountDisplays[address] + } + + /// Returns the types of supported views - none at this time + /// + pub fun getViews(): [Type] { + return [] + } + + /// Resolves the given view if supported - none at this time + /// + pub fun resolveView(_ view: Type): AnyStruct? { + return nil + } + + init(filter: Capability<&{CapabilityFilter.Filter}>?) { + pre { + filter == nil || filter!.check(): "Invalid CapabilityFilter Filter capability provided" + } + self.childAccounts = {} + self.ownedAccounts = {} + self.childAccountDisplays = {} + self.filter = filter + + self.data = {} + self.resources <- {} + } + + destroy () { + destroy self.resources + } + } + + /// The ChildAccount resource sits between a child account and a parent and is stored on the same account as the + /// child account. Once created, a private capability to the child account is shared with the intended parent. The + /// parent account will accept this child capability into its own manager resource and use it to interact with the + /// child account. + /// + /// Because the ChildAccount resource exists on the child account itself, whoever owns the child account will be + /// 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 { + /// A Capability providing access to the underlying child account + access(self) let childCap: Capability<&{BorrowableAccount, OwnedAccountPublic, MetadataViews.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}> + + /// 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}> + + /// 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}> + + /// 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 + /// manager allows access first. + access(self) var managerCapabilityFilter: Capability<&{CapabilityFilter.Filter}>? + + /// A bucket of structs so that the ChildAccount resource can be easily extended with new functionality. + access(self) let data: {String: AnyStruct} + + /// A bucket of resources so that the ChildAccount resource can be easily extended with new functionality. + access(self) let resources: @{String: AnyResource} + + /// 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 + + /// Returns the Address of the underlying child account + /// + pub fun getAddress(): Address { + return self.childCap.address + } + + /// Callback setting the child account as redeemed by the provided parent Address + /// + access(contract) fun redeemedCallback(_ addr: Address) { + self.childCap.borrow()!.setRedeemed(addr) + } + + /// Sets the given filter as the managerCapabilityFilter for this ChildAccount + /// + access(contract) fun setManagerCapabilityFilter( + _ managerCapabilityFilter: Capability<&{CapabilityFilter.Filter}>? + ) { + self.managerCapabilityFilter = managerCapabilityFilter + } + + /// Sets the CapabiltyFactory.Manager Capability + /// + pub fun setCapabilityFactory(cap: Capability<&CapabilityFactory.Manager{CapabilityFactory.Getter}>) { + self.factory = cap + } + + /// Sets the Filter Capability as the one provided + /// + pub 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. 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? { + 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() + let cap = f!.getCapability(acct: acct, path: path) + + // Check that private capabilities are allowed by either internal or manager filter (if assigned) + // If not allowed, return nil + if path.getType() == Type() && ( + self.filter.borrow()!.allowed(cap: cap) == false || + (self.getManagerCapabilityFilter()?.allowed(cap: cap) ?? true) == false + ) { + return nil + } + + return cap + } + + /// 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? { + if let d = self.delegator.borrow() { + return d.getPrivateCapability(type) + } + + return nil + } + + /// 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? { + if let d = self.delegator.borrow() { + return d.getPublicCapability(type) + } + return nil + } + + /// 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) + } + + /// Returns a reference to the stored managerCapabilityFilter if one exists + /// + pub 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) + } + } + + /// Returns a reference to the stored delegator, generally used for arbitrary Capability retrieval + /// + pub fun borrowCapabilityDelegator(): &CapabilityDelegator.Delegator? { + let path = HybridCustody.getCapabilityDelegatorIdentifier(self.parent) + return self.childCap.borrow()!.borrowAccount().borrow<&CapabilityDelegator.Delegator>( + from: StoragePath(identifier: path)! + ) + } + + /// Returns a list of supported metadata views + /// + pub fun getViews(): [Type] { + return [ + Type() + ] + } + + /// Resolves a view of the given type if supported + /// + pub 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) + + if !manager.check() { + return nil + } + + return manager!.borrow()!.getChildAccountDisplay(address: childAddress) + } + return nil + } + + /// Callback to enable parent-initiated removal all the child account and its associated resources & + /// Capabilities + /// + access(contract) fun parentRemoveChildCallback(parent: Address) { + if !self.childCap.check() { + return + } + + let child: &AnyResource{HybridCustody.BorrowableAccount} = self.childCap.borrow()! + if !child.check() { + return + } + + let acct = child.borrowAccount() + if let ownedAcct = acct.borrow<&OwnedAccount>(from: HybridCustody.OwnedAccountStoragePath) { + ownedAcct.removeParent(parent: parent) + } + } + + init( + _ childCap: Capability<&{BorrowableAccount, OwnedAccountPublic, MetadataViews.Resolver}>, + _ factory: Capability<&CapabilityFactory.Manager{CapabilityFactory.Getter}>, + _ filter: Capability<&{CapabilityFilter.Filter}>, + _ delegator: Capability<&CapabilityDelegator.Delegator{CapabilityDelegator.GetterPublic, CapabilityDelegator.GetterPrivate}>, + _ parent: Address + ) { + pre { + childCap.check(): "Provided childCap Capability is invalid" + factory.check(): "Provided factory Capability is invalid" + filter.check(): "Provided filter Capability is invalid" + delegator.check(): "Provided delegator Capability is invalid" + } + self.childCap = childCap + self.factory = factory + self.filter = filter + self.delegator = delegator + self.managerCapabilityFilter = nil // this will get set when a parent account redeems + self.parent = parent + + self.data = {} + self.resources <- {} + } + + /// Returns a capability to this child account's CapabilityFilter + /// + pub fun getCapabilityFilter(): &{CapabilityFilter.Filter}? { + return self.filter.check() ? self.filter.borrow() : nil + } + + /// Returns a capability to this child account's CapabilityFactory + /// + pub fun getCapabilityFactoryManager(): &{CapabilityFactory.Getter}? { + return self.factory.check() ? self.factory.borrow() : nil + } + + destroy () { + destroy <- self.resources + } + } + + /// A resource which sits on the account it manages to make it easier for apps to configure the behavior they want + /// to permit. An OwnedAccount can be used to create ChildAccount resources and share them, publishing them to + /// other addresses. + /// + /// The OwnedAccount can also be used to pass ownership of an account off to another address, or to relinquish + /// ownership entirely, marking the account as owned by no one. Note that even if there isn't an owner, the parent + /// 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 { + /// Capability on the underlying account object + access(self) var acct: Capability<&AuthAccount> + + /// Mapping of current and pending parents, true and false respectively + pub let parents: {Address: Bool} + /// Address of the pending owner, if one exists + pub var pendingOwner: Address? + /// Address of the current owner, if one exists + pub var acctOwner: Address? + /// Owned status of this account + pub 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} + + /// A bucket of resources so that the OwnedAccount resource can be easily extended with new functionality. + access(self) let resources: @{String: AnyResource} + + /// display is its own field on the OwnedAccount resource because only the owner of the child account should be + /// able to set this field. + access(self) var display: MetadataViews.Display? + + /// Callback that sets this OwnedAccount as redeemed by the parent + /// + access(contract) fun setRedeemed(_ addr: Address) { + pre { + self.parents[addr] != nil: "address is not waiting to be redeemed" + } + + self.parents[addr] = true + } + + /// Callback that sets the owner once redeemed + /// + access(contract) fun setOwnerCallback(_ addr: Address) { + pre { + self.pendingOwner == addr: "Address does not match pending owner!" + } + self.pendingOwner = nil + self.acctOwner = addr + } + + + /// A helper method to make it easier to manage what parents an account has configured. The steps to sharing this + /// OwnedAccount with a new parent are: + /// + /// 1. Create a new CapabilityDelegator for the ChildAccount resource being created. We make a new one here because + /// CapabilityDelegator types are meant to be shared explicitly. Making one shared base-line of capabilities might + /// introduce unforseen behavior where an app accidentally shared something to all accounts when it only meant + /// to go to one of them. It is better for parent accounts to have less access than they might have anticipated, + /// than for a child to have given out access it did not intend to. + /// 2. Create a new Capability<&{BorrowableAccount}> which has its own unique path for the parent to share this + /// child account with. We make new ones each time so that you can revoke access from one parent, without + /// destroying them all. A new link is made each time based on the address being shared to allow this + /// fine-grained control, but it is all managed by the OwnedAccount resource itself. + /// 3. A new @ChildAccount resource is created and saved, using the CapabilityDelegator made in step one, and our + /// CapabilityFactory and CapabilityFilter Capabilities. Once saved, public and private links are configured for + /// the ChildAccount. + /// 4. Publish the newly made private link to the designated parent's inbox for them to claim on their @Manager + /// resource. + /// + pub fun publishToParent( + parentAddress: Address, + factory: Capability<&CapabilityFactory.Manager{CapabilityFactory.Getter}>, + filter: Capability<&{CapabilityFilter.Filter}> + ) { + pre{ + self.parents[parentAddress] == nil: "Address pending or already redeemed as parent" + } + let capDelegatorIdentifier = HybridCustody.getCapabilityDelegatorIdentifier(parentAddress) + + let identifier = HybridCustody.getChildAccountIdentifier(parentAddress) + let childAccountStorage = StoragePath(identifier: identifier)! + + 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") + + if acct.borrow<&CapabilityDelegator.Delegator>(from: capDelegatorStorage) == nil { + let delegator <- CapabilityDelegator.createDelegator() + acct.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 + ) + assert(delegator.check(), message: "failed to setup capability delegator for parent address") + + let borrowableCap = self.borrowAccount().getCapability<&{BorrowableAccount, OwnedAccountPublic, MetadataViews.Resolver}>( + HybridCustody.OwnedAccountPrivatePath + ) + let childAcct <- create ChildAccount(borrowableCap, factory, filter, delegator, parentAddress) + + let childAccountPrivatePath = PrivatePath(identifier: identifier)! + + 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) + assert(delegatorCap.check(), message: "Delegator capability check failed") + + acct.inbox.publish(delegatorCap, name: identifier, recipient: parentAddress) + self.parents[parentAddress] = false + + emit ChildAccountPublished( + ownedAcctID: self.uuid, + childAcctID: delegatorCap.borrow()!.uuid, + capDelegatorID: delegator.borrow()!.uuid, + factoryID: factory.borrow()!.uuid, + filterID: filter.borrow()!.uuid, + filterType: filter.borrow()!.getType(), + child: self.getAddress(), + pendingParent: parentAddress + ) + } + + /// Checks the validity of the encapsulated account Capability + /// + pub fun check(): Bool { + return self.acct.check() + } + + /// Returns a reference to the encapsulated account object + /// + pub fun borrowAccount(): &AuthAccount { + return self.acct.borrow()! + } + + /// Returns the addresses of all associated parents pending and active + /// + pub fun getParentAddresses(): [Address] { + return self.parents.keys + } + + /// Returns whether the given address is a parent of this account + /// + pub 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? { + return self.parents[addr] + } + + /// Returns associated parent addresses and their redeemed status + /// + pub fun getParentStatuses(): {Address: Bool} { + return self.parents + } + + /// Unlinks all paths configured when publishing an account, and destroy's the @ChildAccount resource + /// 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 { + 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)!) + + destroy <- acct.load<@AnyResource>(from: StoragePath(identifier: identifier)!) + destroy <- acct.load<@AnyResource>(from: StoragePath(identifier: capDelegatorIdentifier)!) + + 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) + if parentManager.check() { + parentManager.borrow()?.removeParentCallback(child: self.owner!.address) + } + + return true + } + + /// Returns the address of the encapsulated account + /// + pub 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? { + 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? { + if !self.currentlyOwned { + return nil + } + return self.acctOwner != nil ? self.acctOwner! : self.owner!.address + } + + /// This method is used to transfer ownership of the child account to a new address. + /// Ownership here means that one has unrestricted access on this OwnedAccount resource, giving them full + /// access to the account. + /// + /// **NOTE:** The existence of this method does not imply that it is the only way to receive access to a + /// OwnedAccount Capability or that only the labeled 'acctOwner' has said access. Rather, this is a convenient + /// 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) { + 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") + + acct.inbox.publish(cap, name: identifier, recipient: to) + + self.pendingOwner = to + self.currentlyOwned = true + + emit OwnershipGranted(ownedAcctID: self.uuid, child: self.acct.address, previousOwner: self.getOwner(), pendingOwner: to) + } + + /// Revokes all keys on the underlying account + /// + pub fun revokeAllKeys() { + let acct = self.borrowAccount() + + // Revoke all keys + acct.keys.forEach(fun (key: AccountKey): Bool { + if !key.isRevoked { + acct.keys.revoke(keyIndex: key.keyIndex) + } + return true + }) + } + + /// Cancels all existing AuthAccount capabilities, and creates a new one. The newly created capability will + /// then be used by the child account for accessing its AuthAccount going forward. + /// + /// This is used when altering ownership of an account, and can also be used as a safeguard for anyone who + /// 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() { + 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 + }) + + // 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()) + .concat(unsafeRandom().toString()) // ensure that the path is different from the previous one + let acctCap = acct.linkAccount(PrivatePath(identifier: authAcctPath)!)! + + 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) + } + } + + /// 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 parent + /// accounts can still operate normally. + /// Unless this method is executed via the giveOwnership function, this will leave an account **without** an owner. + /// + /// USE WITH EXTREME CAUTION. + /// + pub 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) + self.currentlyOwned = false + } + + /// Retrieves a reference to the ChildAccount associated with the given parent account if one exists. + /// + pub fun borrowChildAccount(parent: Address): &ChildAccount? { + let identifier = HybridCustody.getChildAccountIdentifier(parent) + return self.borrowAccount().borrow<&ChildAccount>(from: StoragePath(identifier: identifier)!) + } + + /// Sets the CapabilityFactory Manager for the specified parent in the associated ChildAccount. + /// + pub fun setCapabilityFactoryForParent( + parent: Address, + cap: Capability<&CapabilityFactory.Manager{CapabilityFactory.Getter}> + ) { + let p = self.borrowChildAccount(parent: parent) ?? panic("could not find parent address") + p.setCapabilityFactory(cap: cap) + } + + /// Sets the Filter for the specified parent in the associated ChildAccount. + /// + pub 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? { + let identifier = HybridCustody.getCapabilityDelegatorIdentifier(parent) + return self.borrowAccount().borrow<&CapabilityDelegator.Delegator>(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) { + 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.addCapability(cap: cap, isPublic: isPublic) + } + + /// Removes the provided Capability from the Delegator associated with the given parent account. + /// + pub 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] { + return [ + Type() + ] + } + + pub fun resolveView(_ view: Type): AnyStruct? { + switch view { + case Type(): + return self.display + } + return nil + } + + /// Sets this OwnedAccount's display to the one provided + /// + pub fun setDisplay(_ d: MetadataViews.Display) { + self.display = d + } + + init( + _ acct: Capability<&AuthAccount> + ) { + self.acct = acct + + self.parents = {} + self.pendingOwner = nil + self.acctOwner = nil + self.currentlyOwned = true + + self.data = {} + self.resources <- {} + self.display = nil + } + + destroy () { + destroy <- self.resources + } + } + + /// 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 { + 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 { + 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 { + return "HybridCustodyOwnedAccount_".concat(HybridCustody.account.address.toString()).concat(addr.toString()) + } + + /// Returns an OwnedAccount wrapping the provided AuthAccount Capability. + /// + pub fun createOwnedAccount( + acct: Capability<&AuthAccount> + ): @OwnedAccount { + pre { + acct.check(): "invalid auth account capability" + } + + let ownedAcct <- create OwnedAccount(acct) + emit CreatedOwnedAccount(id: ownedAcct.uuid, child: acct.borrow()!.address) + return <- ownedAcct + } + + /// Returns a new Manager with the provided Filter as default (if not nil). + /// + pub fun createManager(filter: Capability<&{CapabilityFilter.Filter}>?): @Manager { + pre { + filter == nil || filter!.check(): "Invalid CapabilityFilter Filter capability provided" + } + let manager <- create Manager(filter: filter) + emit CreatedManager(id: manager.uuid) + return <- manager + } + + 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)! + } +} diff --git a/contracts/hybrid-custody/factories/FTAllFactory.cdc b/contracts/hybrid-custody/factories/FTAllFactory.cdc new file mode 100644 index 0000000..46f506b --- /dev/null +++ b/contracts/hybrid-custody/factories/FTAllFactory.cdc @@ -0,0 +1,10 @@ +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.Receiver, FungibleToken.Balance}>(path) + } + } +} \ No newline at end of file diff --git a/contracts/hybrid-custody/factories/FTBalanceFactory.cdc b/contracts/hybrid-custody/factories/FTBalanceFactory.cdc new file mode 100644 index 0000000..bd9d097 --- /dev/null +++ b/contracts/hybrid-custody/factories/FTBalanceFactory.cdc @@ -0,0 +1,10 @@ +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) + } + } +} \ No newline at end of file diff --git a/contracts/hybrid-custody/factories/FTProviderFactory.cdc b/contracts/hybrid-custody/factories/FTProviderFactory.cdc new file mode 100644 index 0000000..e27dd0a --- /dev/null +++ b/contracts/hybrid-custody/factories/FTProviderFactory.cdc @@ -0,0 +1,10 @@ +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) + } + } +} \ No newline at end of file diff --git a/contracts/hybrid-custody/factories/FTReceiverBalanceFactory.cdc b/contracts/hybrid-custody/factories/FTReceiverBalanceFactory.cdc new file mode 100644 index 0000000..49673d5 --- /dev/null +++ b/contracts/hybrid-custody/factories/FTReceiverBalanceFactory.cdc @@ -0,0 +1,10 @@ +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) + } + } +} \ No newline at end of file diff --git a/contracts/hybrid-custody/factories/FTReceiverFactory.cdc b/contracts/hybrid-custody/factories/FTReceiverFactory.cdc new file mode 100644 index 0000000..03ade5c --- /dev/null +++ b/contracts/hybrid-custody/factories/FTReceiverFactory.cdc @@ -0,0 +1,10 @@ +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) + } + } +} \ No newline at end of file diff --git a/contracts/hybrid-custody/factories/NFTCollectionPublicFactory.cdc b/contracts/hybrid-custody/factories/NFTCollectionPublicFactory.cdc new file mode 100644 index 0000000..0e9df83 --- /dev/null +++ b/contracts/hybrid-custody/factories/NFTCollectionPublicFactory.cdc @@ -0,0 +1,10 @@ +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) + } + } +} \ 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..a79755e --- /dev/null +++ b/contracts/hybrid-custody/factories/NFTProviderAndCollectionFactory.cdc @@ -0,0 +1,10 @@ +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 new file mode 100644 index 0000000..7bf547e --- /dev/null +++ b/contracts/hybrid-custody/factories/NFTProviderFactory.cdc @@ -0,0 +1,10 @@ +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) + } + } +} \ No newline at end of file diff --git a/flow.json b/flow.json index 2732976..ace6f55 100644 --- a/flow.json +++ b/flow.json @@ -1,51 +1,193 @@ { - "emulators": { - "default": { - "port": 3569, - "serviceAccount": "emulator-account" - } - }, "contracts": { - "NFTStorefront": "./contracts/NFTStorefront.cdc", - "NFTStorefrontV2": "./contracts/NFTStorefrontV2.cdc", - "ViewResolver": "./contracts/ViewResolver.cdc", + "CapabilityDelegator": { + "source": "./contracts/hybrid-custody/CapabilityDelegator.cdc", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "mainnet": "d8a7e05a7ac670c0", + "testnet": "294e44e1ec6993c6" + } + }, + "CapabilityFactory": { + "source": "./contracts/hybrid-custody/CapabilityFactory.cdc", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "mainnet": "d8a7e05a7ac670c0", + "testnet": "294e44e1ec6993c6" + } + }, + "CapabilityFilter": { + "source": "./contracts/hybrid-custody/CapabilityFilter.cdc", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "mainnet": "d8a7e05a7ac670c0", + "testnet": "294e44e1ec6993c6" + } + }, + "ExampleNFT": "./contracts/utility/ExampleNFT.cdc", + "FlowToken": { + "source": "./contracts/standard/FlowToken.cdc", + "aliases": { + "emulator": "0ae53cb6e3f42a79", + "mainnet": "1654653399040a61", + "testnet": "7e60df042a9c0868" + } + }, + "FTAllFactory": { + "source": "./contracts/hybrid-custody/factories/FTAllFactory.cdc", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "mainnet": "d8a7e05a7ac670c0", + "testnet": "294e44e1ec6993c6" + } + }, + "FTBalanceFactory": { + "source": "./contracts/hybrid-custody/factories/FTBalanceFactory.cdc", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "mainnet": "d8a7e05a7ac670c0", + "testnet": "294e44e1ec6993c6" + } + }, + "FTProviderFactory": { + "source": "./contracts/hybrid-custody/factories/FTProviderFactory.cdc", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "mainnet": "d8a7e05a7ac670c0", + "testnet": "294e44e1ec6993c6" + } + }, + "FTReceiverBalanceFactory": { + "source": "./contracts/hybrid-custody/factories/FTReceiverBalanceFactory.cdc", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "mainnet": "d8a7e05a7ac670c0", + "testnet": "294e44e1ec6993c6" + } + }, + "FTReceiverFactory": { + "source": "./contracts/hybrid-custody/factories/FTReceiverFactory.cdc", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "mainnet": "d8a7e05a7ac670c0", + "testnet": "294e44e1ec6993c6" + } + }, "FungibleToken": { "source": "./contracts/utility/FungibleToken.cdc", "aliases": { - "emulator": "0xee82856bf20e2aa6", - "testnet": "0x9a0766d93b6608b7" + "emulator": "ee82856bf20e2aa6", + "testnet": "9a0766d93b6608b7" } }, - "NonFungibleToken": { - "source": "./contracts/utility/NonFungibleToken.cdc", + "HybridCustody": { + "source": "./contracts/hybrid-custody/HybridCustody.cdc", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "mainnet": "d8a7e05a7ac670c0", + "testnet": "294e44e1ec6993c6" + } + }, + "MetadataViews": { + "source": "./contracts/utility/MetadataViews.cdc", "aliases": { - "emulator": "0xf8d6e0586b0a20c7", - "testnet": "0x631e88ae7f1d7c20" + "emulator": "f8d6e0586b0a20c7", + "testnet": "631e88ae7f1d7c20" } }, "NFTCatalog": { "source": "./contracts/utility/NFTCatalog.cdc", "aliases": { - "emulator": "0xf8d6e0586b0a20c7", - "testnet": "0x324c34e1c517e4db", - "mainnet": "0x49a7cda3a1eecc29" + "emulator": "f8d6e0586b0a20c7", + "mainnet": "49a7cda3a1eecc29", + "testnet": "324c34e1c517e4db" + } + }, + "NFTCollectionPublicFactory": { + "source": "./contracts/hybrid-custody/factories/NFTCollectionPublicFactory.cdc", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "mainnet": "d8a7e05a7ac670c0", + "testnet": "294e44e1ec6993c6" + } + }, + "NFTProviderAndCollectionFactory": { + "source": "./contracts/hybrid-custody/factories/NFTProviderAndCollectionFactory.cdc", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "mainnet": "d8a7e05a7ac670c0", + "testnet": "294e44e1ec6993c6" + } + }, + "NFTProviderFactory": { + "source": "./contracts/hybrid-custody/factories/NFTProviderFactory.cdc", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "testnet": "294e44e1ec6993c6" + } + }, + "NFTStorefront": "./contracts/NFTStorefront.cdc", + "NFTStorefrontV2": "./contracts/NFTStorefrontV2.cdc", + "NonFungibleToken": { + "source": "./contracts/utility/NonFungibleToken.cdc", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "testnet": "631e88ae7f1d7c20" + } + }, + "ViewResolver": { + "source": "./contracts/utility/ViewResolver.cdc", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "testnet": "631e88ae7f1d7c20" } } }, - "networks": { - "emulator": "127.0.0.1:3569", - "mainnet": "access.mainnet.nodes.onflow.org:9000", - "testnet": "access.devnet.nodes.onflow.org:9000" - }, "accounts": { "emulator-account": { - "address": "0xf8d6e0586b0a20c7", - "key": "$FLOW_EMULATOR_PRIVATE_KEY" + "address": "f8d6e0586b0a20c7", + "key": { + "location": "emulator-account.pkey", + "type": "file" + } + }, + "emulator-ft": { + "address": "ee82856bf20e2aa6", + "key": { + "location": "emulator-ft.pkey", + "type": "file" + } } }, "deployments": { "emulator": { - "emulator-account": ["FungibleToken", "NonFungibleToken", "NFTStorefrontV2"] + "emulator-account": [ + "NonFungibleToken", + "MetadataViews", + "ViewResolver", + "ExampleNFT", + "NFTStorefrontV2", + "HybridCustody", + "CapabilityDelegator", + "CapabilityFilter", + "CapabilityFactory", + "FTProviderFactory", + "FTAllFactory", + "FTBalanceFactory", + "FTReceiverBalanceFactory", + "FTReceiverFactory", + "NFTProviderFactory", + "NFTProviderAndCollectionFactory", + "NFTCollectionPublicFactory" + ], + "emulator-ft": [ + "FungibleToken" + ] } + }, + "networks": { + "emulator": "127.0.0.1:3569", + "mainnet": "access.mainnet.nodes.onflow.org:9000", + "testnet": "access.devnet.nodes.onflow.org:9000" } -} +} \ No newline at end of file From 7f4b9368d069a3a9de53ab6b23ce942dd38f358d Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Fri, 8 Dec 2023 17:28:24 -0600 Subject: [PATCH 4/5] add ExampleNFT transactions --- transactions/example-nft/mint_nft.cdc | 90 ++++++++++++++++++++++ transactions/example-nft/setup_account.cdc | 40 ++++++++++ transactions/example-nft/transfer_nft.cdc | 44 +++++++++++ 3 files changed, 174 insertions(+) create mode 100644 transactions/example-nft/mint_nft.cdc create mode 100644 transactions/example-nft/setup_account.cdc create mode 100644 transactions/example-nft/transfer_nft.cdc diff --git a/transactions/example-nft/mint_nft.cdc b/transactions/example-nft/mint_nft.cdc new file mode 100644 index 0000000..51f41a6 --- /dev/null +++ b/transactions/example-nft/mint_nft.cdc @@ -0,0 +1,90 @@ +/// This script uses the NFTMinter resource to mint a new NFT +/// It must be run with the account that has the minter resource +/// stored in /storage/NFTMinter + +import "NonFungibleToken" +import "ExampleNFT" +import "MetadataViews" +import "FungibleToken" + +transaction( + recipient: Address, + name: String, + description: String, + thumbnail: String, + cuts: [UFix64], + royaltyDescriptions: [String], + royaltyBeneficiaries: [Address] +) { + + /// local variable for storing the minter reference + let minter: &ExampleNFT.NFTMinter + + /// Reference to the receiver's collection + let recipientCollectionRef: &{NonFungibleToken.CollectionPublic} + + /// Previous NFT ID before the transaction executes + let mintingIDBefore: UInt64 + + prepare(signer: AuthAccount) { + self.mintingIDBefore = ExampleNFT.totalSupply + + // borrow a reference to the NFTMinter resource in storage + self.minter = signer.borrow<&ExampleNFT.NFTMinter>(from: ExampleNFT.MinterStoragePath) + ?? panic("Account does not store an object at the specified path") + + // Borrow the recipient's public NFT collection reference + self.recipientCollectionRef = getAccount(recipient).getCapability<&{NonFungibleToken.CollectionPublic}>( + ExampleNFT.CollectionPublicPath + ).borrow() + ?? panic("Could not get receiver reference to the NFT Collection") + } + + pre { + cuts.length == royaltyDescriptions.length && cuts.length == royaltyBeneficiaries.length: + "Array length should be equal for royalty related details" + } + + execute { + + // Create the royalty details + var count = 0 + var royalties: [MetadataViews.Royalty] = [] + while royaltyBeneficiaries.length > count { + let beneficiary = royaltyBeneficiaries[count] + let beneficiaryCapability = getAccount(beneficiary).getCapability<&{FungibleToken.Receiver}>( + MetadataViews.getRoyaltyReceiverPublicPath() + ) + + // Make sure the royalty capability is valid before minting the NFT + assert(beneficiaryCapability.check(), message: "Beneficiary capability is not valid!") + + royalties.append( + MetadataViews.Royalty( + receiver: beneficiaryCapability, + cut: cuts[count], + description: royaltyDescriptions[count] + ) + ) + count = count + 1 + } + + + + // Mint the NFT and deposit it to the recipient's collection + self.minter.mintNFT( + recipient: self.recipientCollectionRef, + name: name, + description: description, + thumbnail: thumbnail, + royalties: royalties + ) + } + + post { + self.recipientCollectionRef.getIDs().contains(self.mintingIDBefore): + "The next NFT ID should have been minted and delivered" + ExampleNFT.totalSupply == self.mintingIDBefore + 1: + "The total supply should have been increased by 1" + } +} diff --git a/transactions/example-nft/setup_account.cdc b/transactions/example-nft/setup_account.cdc new file mode 100644 index 0000000..f56bf2f --- /dev/null +++ b/transactions/example-nft/setup_account.cdc @@ -0,0 +1,40 @@ +/// This transaction is what an account would run +/// to set itself up to receive NFTs + +import "NonFungibleToken" +import "ExampleNFT" +import "MetadataViews" + +transaction { + + prepare(signer: AuthAccount) { + // Return early if the account already has a collection + if signer.borrow<&ExampleNFT.Collection>(from: ExampleNFT.CollectionStoragePath) == nil { + // Create a new empty collection + let collection <- ExampleNFT.createEmptyCollection() + + // save it to the account + signer.save(<-collection, to: ExampleNFT.CollectionStoragePath) + } + + // create a public capability for the collection + if signer.getCapability<&{NonFungibleToken.CollectionPublic, ExampleNFT.ExampleNFTCollectionPublic, MetadataViews.ResolverCollection}>( + ExampleNFT.CollectionPublicPath + ).check() == false { + signer.unlink(ExampleNFT.CollectionPublicPath) + signer.link<&{NonFungibleToken.CollectionPublic, ExampleNFT.ExampleNFTCollectionPublic, MetadataViews.ResolverCollection}>( + ExampleNFT.CollectionPublicPath, + target: ExampleNFT.CollectionStoragePath + ) + } + + let providerPath: PrivatePath = /private/exampleNFTProvider + if signer.getCapability<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>(providerPath).check() == false { + signer.unlink(/private/exampleNFTProvider) + signer.link<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>( + providerPath, + target: ExampleNFT.CollectionStoragePath + ) + } + } +} diff --git a/transactions/example-nft/transfer_nft.cdc b/transactions/example-nft/transfer_nft.cdc new file mode 100644 index 0000000..80bb712 --- /dev/null +++ b/transactions/example-nft/transfer_nft.cdc @@ -0,0 +1,44 @@ +/// This transaction is for transferring and NFT from +/// one account to another + +import "NonFungibleToken" +import "ExampleNFT" + +transaction(recipient: Address, withdrawID: UInt64) { + + /// Reference to the withdrawer's collection + let withdrawRef: &ExampleNFT.Collection + + /// Reference of the collection to deposit the NFT to + let depositRef: &{NonFungibleToken.CollectionPublic} + + prepare(signer: AuthAccount) { + // borrow a reference to the signer's NFT collection + self.withdrawRef = signer.borrow<&ExampleNFT.Collection>(from: ExampleNFT.CollectionStoragePath) + ?? panic("Account does not store an object at the specified path") + + // get the recipients public account object + let recipient = getAccount(recipient) + + // borrow a public reference to the receivers collection + self.depositRef = recipient.getCapability<&{NonFungibleToken.CollectionPublic}>( + ExampleNFT.CollectionPublicPath + ).borrow() + ?? panic("Could not borrow a reference to the receiver's collection") + + } + + execute { + + // withdraw the NFT from the owner's collection + let nft <- self.withdrawRef.withdraw(withdrawID: withdrawID) + + // Deposit the NFT in the recipient's collection + self.depositRef.deposit(token: <-nft) + } + + post { + !self.withdrawRef.getIDs().contains(withdrawID): "Original owner should not have the NFT anymore" + self.depositRef.getIDs().contains(withdrawID): "The reciever should now own the NFT" + } +} From 12760b4ad00f4ab95992768671e6f6fd0da3b80b Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Fri, 8 Dec 2023 17:30:31 -0600 Subject: [PATCH 5/5] add HybridCustody setup & cross-account listing txns --- .../sell_item_in_child_from_parent.cdc | 118 ++++++++++++++++++ .../setup_nft_filter_and_factory_manager.cdc | 99 +++++++++++++++ .../setup/linking/redeem_account.cdc | 33 +++++ ...up_owned_account_and_publish_to_parent.cdc | 61 +++++++++ 4 files changed, 311 insertions(+) create mode 100644 transactions/hybrid-custody/sell_item_in_child_from_parent.cdc create mode 100644 transactions/hybrid-custody/setup/dev-setup/setup_nft_filter_and_factory_manager.cdc create mode 100644 transactions/hybrid-custody/setup/linking/redeem_account.cdc create mode 100644 transactions/hybrid-custody/setup/linking/setup_owned_account_and_publish_to_parent.cdc diff --git a/transactions/hybrid-custody/sell_item_in_child_from_parent.cdc b/transactions/hybrid-custody/sell_item_in_child_from_parent.cdc new file mode 100644 index 0000000..38e88eb --- /dev/null +++ b/transactions/hybrid-custody/sell_item_in_child_from_parent.cdc @@ -0,0 +1,118 @@ +import "NonFungibleToken" +import "MetadataViews" +import "FungibleToken" +import "FlowToken" + +import "HybridCustody" + +import "NFTStorefrontV2" + +/// Cross-account NFT listing transaction +/// +/// Lists an NFT located in the signer's child account for sale in the storefront of the signing parent account with +/// the parent account as beneficiary of the sale. +/// +transaction( + childAddress: Address, + collectionProviderPath: PrivatePath, + collectionPublicPath: PublicPath, + nftTypeIdentifier: String, + saleItemID: UInt64, + saleItemPrice: UFix64, + customID: String?, + commissionAmount: UFix64, + expiry: UInt64, + marketplacesAddress: [Address] +) { + let flowReceiverCap: Capability<&{FungibleToken.Receiver}> + let providerCap: Capability<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}> + let storefront: &NFTStorefrontV2.Storefront + var saleCuts: [NFTStorefrontV2.SaleCut] + var marketplaceCaps: [Capability<&{FungibleToken.Receiver}>] + let nftType: Type + + prepare(acct: AuthAccount) { + self.saleCuts = [] + self.marketplaceCaps = [] + self.nftType = CompositeType(nftTypeIdentifier) ?? panic("Invalid NFT Type Identifier provided") + + // Configure Storefront if one doesn't yet exist + if acct.borrow<&NFTStorefrontV2.Storefront>(from: NFTStorefrontV2.StorefrontStoragePath) == nil { + acct.save(<-NFTStorefrontV2.createStorefront(), to: NFTStorefrontV2.StorefrontStoragePath) + acct.link<&NFTStorefrontV2.Storefront{NFTStorefrontV2.StorefrontPublic}>( + NFTStorefrontV2.StorefrontPublicPath, + target: NFTStorefrontV2.StorefrontStoragePath + ) + } + // Borrow a reference to the signer's Storefront + self.storefront = acct.borrow<&NFTStorefrontV2.Storefront>(from: NFTStorefrontV2.StorefrontStoragePath) + ?? panic("Missing or mis-typed NFTStorefront Storefront") + + // Get a FlowToken Receiver as beneficiary of listing & validate + self.flowReceiverCap = acct.getCapability<&{FungibleToken.Receiver}>(/public/flowTokenReceiver) + assert(self.flowReceiverCap.check(), message: "Missing or mis-typed FlowToken receiver") + + // Get reference to the child account + let manager = acct.borrow<&HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath) + ?? panic("Could not borrow reference to HybridCustody Manager") + let childAccount = manager.borrowAccount(addr: childAddress) + ?? panic("No child account exists for the given address") + + // Get the NFT provider capability from the child account & validate + self.providerCap = childAccount.getCapability( + path: collectionProviderPath, + type: Type<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>() + ) as! Capability<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>? + ?? panic("NFT Provider Capability is not accessible from child account for specified path") + assert(self.providerCap.check(), message: "Missing or mis-typed Provider Capability") + + // Borrow the NFT as ViewResolver to get Royalties information + let collection = getAccount(childAddress).getCapability<&{MetadataViews.ResolverCollection}>( + collectionPublicPath + ).borrow() + ?? panic("Could not borrow a reference to the child account's collection") + var totalRoyaltyCut = 0.0 + let effectiveSaleItemPrice = saleItemPrice - commissionAmount + let resolver = collection.borrowViewResolver(id: saleItemID) + assert(resolver.getType() == self.nftType, message: "NFT Type mismatch") + + // Check whether the NFT implements the MetadataResolver or not. + if resolver.getViews().contains(Type()) { + let royaltiesRef = resolver.resolveView(Type())?? panic("Unable to retrieve the royalties") + let royalties = (royaltiesRef as! MetadataViews.Royalties).getRoyalties() + for royalty in royalties { + self.saleCuts.append( + NFTStorefrontV2.SaleCut(receiver: royalty.receiver, amount: royalty.cut * effectiveSaleItemPrice) + ) + totalRoyaltyCut = totalRoyaltyCut + royalty.cut * effectiveSaleItemPrice + } + } + self.saleCuts.append(NFTStorefrontV2.SaleCut( + receiver: self.flowReceiverCap, + amount: effectiveSaleItemPrice - totalRoyaltyCut + )) + + for marketplace in marketplacesAddress { + // Here we are making a fair assumption that all given addresses would have + // the capability to receive the `FlowToken` + self.marketplaceCaps.append( + getAccount(marketplace).getCapability<&{FungibleToken.Receiver}>(/public/flowTokenReceiver) + ) + } + } + + execute { + // Create listing + self.storefront.createListing( + nftProviderCapability: self.providerCap, + nftType: self.nftType, + nftID: saleItemID, + salePaymentVaultType: Type<@FlowToken.Vault>(), + saleCuts: self.saleCuts, + marketplacesCapability: self.marketplaceCaps.length == 0 ? nil : self.marketplaceCaps, + customID: customID, + commissionAmount: commissionAmount, + expiry: expiry + ) + } +} diff --git a/transactions/hybrid-custody/setup/dev-setup/setup_nft_filter_and_factory_manager.cdc b/transactions/hybrid-custody/setup/dev-setup/setup_nft_filter_and_factory_manager.cdc new file mode 100644 index 0000000..8510adf --- /dev/null +++ b/transactions/hybrid-custody/setup/dev-setup/setup_nft_filter_and_factory_manager.cdc @@ -0,0 +1,99 @@ +import "CapabilityFilter" +import "CapabilityFactory" +import "NFTCollectionPublicFactory" +import "NFTProviderAndCollectionFactory" +import "NFTProviderFactory" +import "FTProviderFactory" + +import "NonFungibleToken" +import "FungibleToken" + +/* --- Helper Methods --- */ +// +/// Returns a type identifier for an NFT Collection +/// +access(all) fun deriveCollectionTypeIdentifier(_ contractAddress: Address, _ contractName: String): String { + return "A.".concat(withoutPrefix(contractAddress.toString())).concat(".").concat(contractName).concat(".Collection") +} + +/// Taken from AddressUtils private method +/// +access(all) fun withoutPrefix(_ input: String): String{ + var address=input + + //get rid of 0x + if address.length>1 && address.utf8[1] == 120 { + address = address.slice(from: 2, upTo: address.length) + } + + //ensure even length + if address.length%2==1{ + address="0".concat(address) + } + return address +} + +/* --- Transaction Block --- */ +// +/// This transaction can be used by most developers implementing HybridCustody as the single pre-requisite transaction +/// to setup filter functionality between linked parent and child accounts. +/// +/// Creates a CapabilityFactory Manager and CapabilityFilter.AllowlistFilter in the signing account (if needed), adding +/// NFTCollectionPublicFactory, NFTProviderAndCollectionFactory, & NFTProviderFactory to the CapabilityFactory Manager +/// and the Collection Type to the CapabilityFilter.AllowlistFilter +/// +/// For more info, see docs at https://developers.onflow.org/docs/hybrid-custody/ +//// +transaction(nftContractAddress: Address, nftContractName: String) { + prepare(acct: AuthAccount) { + + /* --- CapabilityFactory Manager configuration --- */ + // + if acct.borrow<&AnyResource>(from: CapabilityFactory.StoragePath) == nil { + let f <- CapabilityFactory.createFactoryManager() + acct.save(<-f, to: CapabilityFactory.StoragePath) + } + + if !acct.getCapability<&CapabilityFactory.Manager{CapabilityFactory.Getter}>(CapabilityFactory.PublicPath).check() { + acct.unlink(CapabilityFactory.PublicPath) + acct.link<&CapabilityFactory.Manager{CapabilityFactory.Getter}>(CapabilityFactory.PublicPath, target: CapabilityFactory.StoragePath) + } + + assert( + acct.getCapability<&CapabilityFactory.Manager{CapabilityFactory.Getter}>(CapabilityFactory.PublicPath).check(), + message: "CapabilityFactory is not setup properly" + ) + + let factoryManager = acct.borrow<&CapabilityFactory.Manager>(from: CapabilityFactory.StoragePath) + ?? panic("CapabilityFactory Manager not found") + + // Add NFT-related Factories to the Manager + factoryManager.updateFactory(Type<&{NonFungibleToken.CollectionPublic}>(), NFTCollectionPublicFactory.Factory()) + factoryManager.updateFactory(Type<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>(), NFTProviderAndCollectionFactory.Factory()) + factoryManager.updateFactory(Type<&{NonFungibleToken.Provider}>(), NFTProviderFactory.Factory()) + + /* --- AllowlistFilter configuration --- */ + // + if acct.borrow<&CapabilityFilter.AllowlistFilter>(from: CapabilityFilter.StoragePath) == nil { + acct.save(<-CapabilityFilter.create(Type<@CapabilityFilter.AllowlistFilter>()), to: CapabilityFilter.StoragePath) + } + + if !acct.getCapability<&CapabilityFilter.AllowlistFilter{CapabilityFilter.Filter}>(CapabilityFilter.PublicPath).check() { + acct.unlink(CapabilityFilter.PublicPath) + acct.link<&CapabilityFilter.AllowlistFilter{CapabilityFilter.Filter}>(CapabilityFilter.PublicPath, target: CapabilityFilter.StoragePath) + } + + assert( + acct.getCapability<&CapabilityFilter.AllowlistFilter{CapabilityFilter.Filter}>(CapabilityFilter.PublicPath).check(), + message: "AllowlistFilter is not setup properly" + ) + + let filter = acct.borrow<&CapabilityFilter.AllowlistFilter>(from: CapabilityFilter.StoragePath) + ?? panic("AllowlistFilter does not exist") + + // Construct an NFT Collection Type from the provided args & add to the AllowlistFilter + let c = CompositeType(deriveCollectionTypeIdentifier(nftContractAddress, nftContractName)) + ?? panic("Problem constructing CompositeType from given NFT contract address and name") + filter.addType(c) + } +} diff --git a/transactions/hybrid-custody/setup/linking/redeem_account.cdc b/transactions/hybrid-custody/setup/linking/redeem_account.cdc new file mode 100644 index 0000000..70016db --- /dev/null +++ b/transactions/hybrid-custody/setup/linking/redeem_account.cdc @@ -0,0 +1,33 @@ +import "MetadataViews" + +import "HybridCustody" +import "CapabilityFilter" + +transaction(childAddress: Address, filterAddress: Address?, filterPath: PublicPath?) { + prepare(acct: AuthAccount) { + var filter: Capability<&{CapabilityFilter.Filter}>? = nil + if filterAddress != nil && filterPath != nil { + filter = getAccount(filterAddress!).getCapability<&{CapabilityFilter.Filter}>(filterPath!) + } + + if acct.borrow<&HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath) == nil { + let m <- HybridCustody.createManager(filter: filter) + acct.save(<- m, to: HybridCustody.ManagerStoragePath) + + acct.unlink(HybridCustody.ManagerPublicPath) + acct.unlink(HybridCustody.ManagerPrivatePath) + + acct.link<&HybridCustody.Manager{HybridCustody.ManagerPrivate, HybridCustody.ManagerPublic}>(HybridCustody.ManagerPrivatePath, target: HybridCustody.ManagerStoragePath) + acct.link<&HybridCustody.Manager{HybridCustody.ManagerPublic}>(HybridCustody.ManagerPublicPath, target: HybridCustody.ManagerStoragePath) + } + + let inboxName = HybridCustody.getChildAccountIdentifier(acct.address) + let cap = acct.inbox.claim<&HybridCustody.ChildAccount{HybridCustody.AccountPrivate, HybridCustody.AccountPublic, MetadataViews.Resolver}>(inboxName, provider: childAddress) + ?? panic("child account cap not found") + + let manager = acct.borrow<&HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath) + ?? panic("manager no found") + + manager.addAccount(cap: cap) + } +} \ No newline at end of file diff --git a/transactions/hybrid-custody/setup/linking/setup_owned_account_and_publish_to_parent.cdc b/transactions/hybrid-custody/setup/linking/setup_owned_account_and_publish_to_parent.cdc new file mode 100644 index 0000000..2349962 --- /dev/null +++ b/transactions/hybrid-custody/setup/linking/setup_owned_account_and_publish_to_parent.cdc @@ -0,0 +1,61 @@ +#allowAccountLinking + +import "MetadataViews" + +import "HybridCustody" +import "CapabilityFactory" +import "CapabilityFilter" +import "CapabilityDelegator" + +/// This transaction configures an OwnedAccount in the signer if needed, and proceeds to create a ChildAccount +/// using CapabilityFactory.Manager and CapabilityFilter.Filter Capabilities from the given addresses. A +/// Capability on the ChildAccount is then published to the specified parent account. +/// +transaction( + parent: Address, + factoryAddress: Address, + filterAddress: Address, + name: String?, + desc: String?, + thumbnailURL: String? + ) { + + prepare(acct: AuthAccount) { + // Configure OwnedAccount if it doesn't exist + if acct.borrow<&HybridCustody.OwnedAccount>(from: HybridCustody.OwnedAccountStoragePath) == nil { + var acctCap = acct.getCapability<&AuthAccount>(HybridCustody.LinkedAccountPrivatePath) + if !acctCap.check() { + acctCap = acct.linkAccount(HybridCustody.LinkedAccountPrivatePath)! + } + let ownedAccount <- HybridCustody.createOwnedAccount(acct: acctCap) + acct.save(<-ownedAccount, to: HybridCustody.OwnedAccountStoragePath) + } + + // check that paths are all configured properly + acct.unlink(HybridCustody.OwnedAccountPrivatePath) + acct.link<&HybridCustody.OwnedAccount{HybridCustody.BorrowableAccount, HybridCustody.OwnedAccountPublic, MetadataViews.Resolver}>(HybridCustody.OwnedAccountPrivatePath, target: HybridCustody.OwnedAccountStoragePath) + + acct.unlink(HybridCustody.OwnedAccountPublicPath) + acct.link<&HybridCustody.OwnedAccount{HybridCustody.OwnedAccountPublic, MetadataViews.Resolver}>(HybridCustody.OwnedAccountPublicPath, target: HybridCustody.OwnedAccountStoragePath) + + let owned = acct.borrow<&HybridCustody.OwnedAccount>(from: HybridCustody.OwnedAccountStoragePath) + ?? panic("owned account not found") + + // Set the display metadata for the OwnedAccount + if name != nil && desc != nil && thumbnailURL != nil { + let thumbnail = MetadataViews.HTTPFile(url: thumbnailURL!) + let display = MetadataViews.Display(name: name!, description: desc!, thumbnail: thumbnail!) + owned.setDisplay(display) + } + + // Get CapabilityFactory & CapabilityFilter Capabilities + let factory = getAccount(factoryAddress).getCapability<&CapabilityFactory.Manager{CapabilityFactory.Getter}>(CapabilityFactory.PublicPath) + assert(factory.check(), message: "factory address is not configured properly") + + let filter = getAccount(filterAddress).getCapability<&{CapabilityFilter.Filter}>(CapabilityFilter.PublicPath) + assert(filter.check(), message: "capability filter is not configured properly") + + // Finally publish a ChildAccount capability on the signing account to the specified parent + owned.publishToParent(parentAddress: parent, factory: factory, filter: filter) + } +} \ No newline at end of file