Smart Contract

FlowtyUtils

A.3cdbb3d569211ff3.FlowtyUtils

Deployed

6d ago
Feb 20, 2026, 04:31:30 AM UTC

Dependents

22 imports
1import FlowToken from 0x1654653399040a61
2import NonFungibleToken from 0x1d7e57aa55817448
3import FungibleToken from 0xf233dcee88fe0abe
4import MetadataViews from 0x1d7e57aa55817448
5import NFTCatalog from 0x49a7cda3a1eecc29
6import LostAndFound from 0x473d6a2c37eab5be
7import StringUtils from 0xa340dc0a4ec828ab
8import RoyaltiesOverride from 0x3cdbb3d569211ff3
9
10
11access(all) contract FlowtyUtils {
12	access(contract) var Attributes: {String: AnyStruct}
13
14	access(all) let FlowtyUtilsStoragePath: StoragePath
15
16	// Deprecated
17	access(all) struct NFTIdentifier {}
18
19	// Deprecated
20	access(all) struct CollectionInfo {}
21
22	access(all) struct TokenInfo {
23		access(all) let tokenType: Type
24		access(all) let storagePath: StoragePath
25		access(all) let balancePath: PublicPath
26		access(all) let receiverPath: PublicPath
27		access(all) let providerPath: PrivatePath
28
29		init(
30			tokenType: Type, 
31			storagePath: StoragePath, 
32			balancePath: PublicPath, 
33			receiverPath: PublicPath, 
34			providerPath: PrivatePath
35		) {
36			self.tokenType = tokenType
37			self.storagePath = storagePath
38			self.balancePath = balancePath
39			self.receiverPath = receiverPath
40			self.providerPath = providerPath
41		}
42	}
43	
44	// Deprecated
45	access(all) struct PaymentCut {}
46
47	access(all) resource FlowtyUtilsAdmin {
48		access(all) fun setBalancePath(key: String, path: PublicPath): Bool {
49			if FlowtyUtils.Attributes["balancePaths"] == nil {
50				FlowtyUtils.Attributes["balancePaths"] = BalancePaths()
51			}
52
53			return (FlowtyUtils.Attributes["balancePaths"]! as! BalancePaths).set(key: key, path: path)
54		}
55
56		// addSupportedTokenType
57		// add a supported token type that can be used in Flowty loans
58		access(all) fun addSupportedTokenType(tokenInfo: TokenInfo) {
59			var supportedTokens = FlowtyUtils.Attributes["supportedTokens"]
60			if supportedTokens == nil {
61				supportedTokens = {} as {Type: TokenInfo}
62			}
63
64			let tokens = supportedTokens! as! {Type: TokenInfo}
65			tokens[tokenInfo.tokenType] = tokenInfo
66			FlowtyUtils.Attributes["supportedTokens"] = tokens
67
68			self.setBalancePath(key: tokenInfo.tokenType.identifier, path: tokenInfo.balancePath)
69		}
70
71		access(all) fun removeSupportedTokenType(type: Type) {
72			let tokens = (FlowtyUtils.Attributes["supportedTokens"] != nil ? FlowtyUtils.Attributes["supportedTokens"]! : {} as {Type: TokenInfo}) as! {Type: TokenInfo}
73			tokens.remove(key: type)
74			FlowtyUtils.Attributes["supportedTokens"] = tokens
75		}
76	}
77
78	access(all) view fun getTokenInfo(_ type: Type): TokenInfo? {
79		let tokens = (FlowtyUtils.Attributes["supportedTokens"] != nil ? FlowtyUtils.Attributes["supportedTokens"]! : {} as {Type: TokenInfo}) as! {Type: TokenInfo}
80		return tokens[type]
81	}
82
83	access(all) view fun getSupportedTokens(): [Type] {
84		let attribute = self.Attributes["supportedTokens"]
85		if attribute == nil {
86			return []
87		}
88		let supportedTokens = attribute! as! {Type: TokenInfo}
89		return supportedTokens.keys
90	}
91
92
93	access(all) view fun getAllBalances(address: Address): {String: UFix64} {
94		let allowedTokens = FlowtyUtils.getSupportedTokens()
95		let balances: {String: UFix64} = {}
96
97		for index, allowedToken in allowedTokens {
98			let vaultType = allowedTokens[index]
99			let balance = FlowtyUtils.getTokenBalance(address: address, vaultType: vaultType)
100			balances[vaultType.identifier] = balance
101		}
102		return balances
103	}
104
105	access(self) fun getDepositor(): auth(LostAndFound.Deposit) &LostAndFound.Depositor {
106		return self.account.storage.borrow<auth(LostAndFound.Deposit) &LostAndFound.Depositor>(from: LostAndFound.DepositorStoragePath)!
107	}
108
109	/*
110		We cannot know the real value of each item being transacted with. Because of that, we will simply split 
111		the royalty rate evenly amongst each MetadataViews.Royalties item, and then split that piece of the royalty
112		evenly amongst each Royalty in that item.
113
114		For example, let's say we have two MetadataRoyalties entries:
115		1. A single cutInfo of 5%
116		2. Two cutInfos of 1% and 5%
117
118		And let's also say that the royaltyRate is 5%. In that scenario, half of the vault goes to each Royalties entry.
119		All of the first half goes to cutInfo #1's destination. The second half should send 1/6 of its half to the first
120		cutInfo, and 5/6 of the second half to the second cutInfo.
121
122		So if we had a loan of 1000 tokens, 25 goes to cutInfo 1, ~4.16 goes to cutInfo2.1, and ~20.4 to cutInfo2.
123	 */
124	access(all) fun metadataRoyaltiesToRoyaltyCuts(tokenInfo: TokenInfo, mdRoyalties: [MetadataViews.Royalties]): [RoyaltyCut] {
125		if mdRoyalties.length == 0 {
126			return []
127		}
128		
129		let royaltyCuts: {Address: RoyaltyCut} = {}
130
131		// the cut for each Royalties object is split evenly, regardless of the hypothetical value
132		// difference between each asset they came from.
133		let cutPerRoyalties = 1.0 / UFix64(mdRoyalties.length)
134
135		// for each royalties struct, calculate the sum totals to go to each benefiary
136		// then roll them up in 
137		for royalties in mdRoyalties {
138			// we need to know the total % taken from this set of royalties so that we can
139			// calculate the total proportion taken from each Royalty struct inside of it. 
140			// Unfortunately there isn't another way to do this since the total cut amount 
141			// isn't pre-populated by the Royalties standard
142			var royaltiesTotal = 0.0
143			for cutInfo in royalties.getRoyalties() {
144				royaltiesTotal = royaltiesTotal + cutInfo.cut
145			}
146
147			for cutInfo in royalties.getRoyalties() {
148				if royaltyCuts[cutInfo.receiver.address] == nil {
149					let cap = getAccount(cutInfo.receiver.address).capabilities.get<&{FungibleToken.Receiver}>(tokenInfo.receiverPath)
150					if cap == nil {
151						continue
152					}
153
154					royaltyCuts[cutInfo.receiver.address] = RoyaltyCut(cap: cap!, percentage: 0.0)
155				}
156
157				let denom = royaltiesTotal * cutPerRoyalties
158				if denom == 0.0 {
159					continue
160				}
161
162				royaltyCuts[cutInfo.receiver.address]!.add(p: cutInfo.cut / denom)
163			}
164		}
165
166		return royaltyCuts.values
167	}
168
169	access(all) struct RoyaltyCut {
170		access(all) let cap: Capability<&{FungibleToken.Receiver}>
171		access(all) var percentage: UFix64
172
173		init(cap: Capability<&{FungibleToken.Receiver}>, percentage: UFix64) {
174			self.cap = cap
175			self.percentage = percentage
176		}
177
178		access(all) fun add(p: UFix64) {
179			self.percentage = self.percentage + p
180		}
181	}
182
183	access(all) fun distributeRoyaltiesWithDepositor(royaltyCuts: [RoyaltyCut], depositor: auth(LostAndFound.Deposit) &LostAndFound.Depositor, vault: @{FungibleToken.Vault}): @{FungibleToken.Vault}? {
184		let depositor = FlowtyUtils.getDepositor()
185		let startBalance = vault.balance
186		for index, rs in royaltyCuts {
187			if index == royaltyCuts.length - 1 {
188				depositor.trySendResource(item: <-vault, cap: rs.cap, memo: "flowty royalty distribution", display: nil)  
189				return nil
190			}
191
192			depositor.trySendResource(item: <-vault.withdraw(amount: startBalance * rs.percentage), cap: rs.cap, memo: "flowty royalty distribution", display: nil)
193		}
194		return <- vault
195	}
196
197	// getAllowedTokens
198	// return an array of types that are able to be used as the payment type
199	// for loans
200	access(all) view fun getAllowedTokens(): [Type] {
201		let tokens = (FlowtyUtils.Attributes["supportedTokens"] != nil ? FlowtyUtils.Attributes["supportedTokens"]! : {} as {Type: TokenInfo}) as! {Type: TokenInfo}
202		return tokens.keys
203	}
204
205	// isTokenSupported
206	// check if the given type is able to be used as payment
207	access(all) view fun isTokenSupported(type: Type): Bool {
208		for t in FlowtyUtils.getAllowedTokens() {
209			if t == type {
210				return true
211			}
212		}
213
214		return false
215	}
216
217	access(all) fun depositToLostAndFound(
218		redeemer: Address,
219		item: @AnyResource,
220		memo: String?,
221		display: MetadataViews.Display?,
222		depositor: auth(LostAndFound.Deposit) &LostAndFound.Depositor
223	) {
224		depositor.deposit(redeemer: redeemer, item: <-item, memo: memo, display: display)
225	}
226
227	access(all) fun trySendFungibleTokenVault(vault: @{FungibleToken.Vault}, receiver: Capability<&{FungibleToken.Receiver}>, depositor: auth(LostAndFound.Deposit) &LostAndFound.Depositor){
228		if !receiver.check() {
229			depositor.deposit(redeemer: receiver.address, item: <-vault, memo: nil, display: nil)
230		} else {
231			receiver.borrow()!.deposit(from: <-vault)
232		}
233	}
234
235	access(all) fun trySendNFT(nft: @{NonFungibleToken.NFT}, receiver: Capability<&{NonFungibleToken.CollectionPublic}>, depositor: auth(LostAndFound.Deposit) &LostAndFound.Depositor) {
236		if !receiver.check() {
237			depositor.deposit(
238				redeemer: receiver.address,
239				item: <- nft,
240				memo: nil,
241				display: nil,
242			)
243		} else {
244			receiver.borrow()!.deposit(token: <-nft)
245		}
246	}
247
248	access(all) struct BalancePaths {
249		access(self) var paths: {String: PublicPath}
250
251		access(account) fun get(key: String): PublicPath? {
252			return self.paths[key]
253		}
254		
255
256		access(account) fun set(key: String, path: PublicPath): Bool {
257			let pathOverwritten = self.paths[key] != nil
258
259			self.paths[key] = path
260
261			return pathOverwritten
262		}
263
264		init() {
265			self.paths = {}
266		}
267	}
268
269	access(all) view fun getTokenBalance(address: Address, vaultType: Type): UFix64 {
270		// get the account for the address we want the balance for
271		let user = getAccount(address)
272
273		// get the balance path for the user for the given fungible token
274		let ti = FlowtyUtils.getTokenInfo(vaultType)
275			?? panic("No configuration for ".concat(vaultType.identifier))
276		let balancePath = ti.balancePath
277
278		assert(balancePath != nil, message: "No balance path configured for ".concat(vaultType.identifier))
279		
280		// get the FungibleToken.Balance capability located at the path
281		let tmp = user.capabilities.get<&{FungibleToken.Balance}>(balancePath)
282		if tmp == nil {
283			return 0.0
284		}
285
286		let vaultCap = tmp!
287		
288		// check the capability exists
289		if !vaultCap.check() {
290			return 0.0
291		}
292
293		// borrow the reference
294		let vaultRef = vaultCap.borrow()
295
296		// get the balance of the account
297		return vaultRef?.balance ?? 0.0
298	}
299
300	access(all) fun getRoyaltyRate(_ nft: &{NonFungibleToken.NFT}): UFix64 {
301		// check for overrides first
302
303		if RoyaltiesOverride.get(nft.getType()) {
304			return 0.0
305		}
306
307		let royalties = nft.resolveView(Type<MetadataViews.Royalties>()) as! MetadataViews.Royalties?
308		if royalties == nil {
309			return 0.0
310		}
311
312		// count the royalty rate now, then we'll pick them all up after the fact when a loan is settled?
313		var total = 0.0
314		for r in royalties!.getRoyalties() {
315			total = total + r.cut
316		}
317
318		return total
319	}
320
321	access(all) fun isSupported(_ nft: &{NonFungibleToken.NFT}): Bool {
322		let collections = NFTCatalog.getCollectionsForType(nftTypeIdentifier: nft.getType().identifier)
323		if collections == nil {
324			return false
325		}
326
327		for v in collections!.values {
328			if v {
329				return true
330			}
331		}
332
333		return false
334	}
335
336	access(all) fun getCapabilityStoragePath(type: Type, suffix: String): StoragePath {
337		let segments = type.identifier.split(separator: ".")
338
339		let pathParts: [String] = [segments[2], "0x".concat(segments[1]), segments[3], suffix]
340		let identifier = StringUtils.join(pathParts, "_")
341		return StoragePath(identifier: identifier) ?? panic("invalid storage path")
342	}
343
344	init() {
345		self.Attributes = {}
346
347		self.FlowtyUtilsStoragePath = /storage/FlowtyUtils
348
349		let utilsAdmin <- create FlowtyUtilsAdmin()
350		self.account.storage.save(<-utilsAdmin, to: self.FlowtyUtilsStoragePath)
351
352		if self.account.storage.borrow<&LostAndFound.Depositor>(from: LostAndFound.DepositorStoragePath) == nil {
353			let flowTokenReceiver = self.account.capabilities.get<&FlowToken.Vault>(/public/flowTokenReceiver)!
354			let depositor <- LostAndFound.createDepositor(flowTokenReceiver, lowBalanceThreshold: 10.0)
355			self.account.storage.save(<-depositor, to: LostAndFound.DepositorStoragePath)
356		}
357	}
358}
359