Smart Contract
FlowtyUtils
A.3cdbb3d569211ff3.FlowtyUtils
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