Skip to content

Commit

Permalink
add HybridCustody setup & cross-account listing txns
Browse files Browse the repository at this point in the history
  • Loading branch information
sisyphusSmiling committed Dec 8, 2023
1 parent 7f4b936 commit 12760b4
Show file tree
Hide file tree
Showing 4 changed files with 311 additions and 0 deletions.
118 changes: 118 additions & 0 deletions transactions/hybrid-custody/sell_item_in_child_from_parent.cdc
Original file line number Diff line number Diff line change
@@ -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<MetadataViews.Royalties>()) {
let royaltiesRef = resolver.resolveView(Type<MetadataViews.Royalties>())?? 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
)
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
33 changes: 33 additions & 0 deletions transactions/hybrid-custody/setup/linking/redeem_account.cdc
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}

0 comments on commit 12760b4

Please sign in to comment.