Smart Contract
Melody
A.32a6af84f2f54476.Melody
1import MelodyError from 0x32a6af84f2f54476
2import FungibleToken from 0xf233dcee88fe0abe
3import NonFungibleToken from 0x1d7e57aa55817448
4import MetadataViews from 0x1d7e57aa55817448
5import MelodyTicket from 0x32a6af84f2f54476
6
7
8pub contract Melody {
9
10 /** ___ ____ ___ _ _ ____
11 * |__] |__| | |__| [__
12 * | | | | | | ___]
13 *************************/
14
15
16 pub let UserCertificateStoragePath: StoragePath
17 pub let UserCertificatePrivatePath: PrivatePath
18 // pub let CollectionStoragePath: StoragePath
19 // pub let CollectionPublicPath: PublicPath
20 // pub let CollectionPrivatePath: PrivatePath
21 pub let AdminStoragePath: StoragePath
22
23 /** ____ _ _ ____ _ _ ___ ____
24 * |___ | | |___ |\ | | [__
25 * |___ \/ |___ | \| | ___]
26 ******************************/
27
28 pub event ContractInitialized()
29 pub event PauseStateChanged(pauseFlag: Bool, operator: Address)
30 pub event GraceDurationChanged(before: UFix64, after: UFix64)
31 pub event MinimumPaymentChanged(before: UFix64, after: UFix64)
32 pub event CommisionChanged(before: UFix64, after: UFix64)
33 pub event TicketCached(paymentId: UInt64, ticketId: UInt64, receiver: Address)
34 pub event TicketClaimed(paymentId: UInt64, ticketId: UInt64, receiver: Address)
35 pub event PaymentConfigUpdated(paymentId: UInt64, key: String,)
36 pub event PaymentRevoked(paymentId: UInt64, amount: UFix64, operator: Address)
37 pub event PaymentStatusUpdated(paymentId: UInt64, oldStatus: UInt8, newStatus: UInt8)
38 pub event PaymentTypeChanged(paymentId: UInt64, oldType: UInt8, newType: UInt8)
39 pub event PaymentDestroyed(paymentId: UInt64, ticketId: UInt64?)
40 pub event PaymentWithdrawn(paymentId: UInt64, type: UInt8, status: UInt8, amount: UFix64)
41 pub event VaultDeposited(identifier:String, amount: UFix64)
42 pub event VaultCreated(identifier: String)
43 pub event VaultWithdrawn(identifier: String, amount: UFix64, balance: UFix64)
44 pub event PaymentRecordsUpdated(address: Address, before: [UInt64], after: [UInt64])
45 pub event PaymentCreated(paymentId: UInt64, type: UInt8, creator: Address, receiver: Address, amount: UFix64 )
46 pub event TicketRecordChanged(address: Address , before: [UInt64], after: [UInt64])
47 pub event CommisionSended(paymentId: UInt64, identifier: String, amount: UFix64)
48
49
50 /** ____ ___ ____ ___ ____
51 * [__ | |__| | |___
52 * ___] | | | | |___
53 ************************/
54
55 // ticket type
56 pub enum PaymentType: UInt8 {
57 pub case STREAM // stream ticke
58 pub case REVOCABLE_STREAM // revocable stream ticket
59 pub case VESTING
60 pub case REVOCABLE_VESTING // revocable vesting ticket
61 }
62
63 // status for payment life cycle
64 pub enum PaymentStatus: UInt8 {
65 pub case UPCOMING // not start yet
66 pub case ACTIVE // running payment
67 pub case COMPLETE // completed payment
68 pub case CANCELED // revoced payment
69 }
70
71 pub var totalCreated: UInt64
72 pub var vestingCount: UInt64
73 pub var streamCount: UInt64
74
75
76 // global pause: true will stop pool creation
77 pub var pause: Bool
78
79 pub var melodyCommision: UFix64
80
81 pub var minimumPayment: UFix64
82
83 pub var graceDuration: UFix64
84
85 // records user unclaim tickets with payments
86 access(account) var userTicketRecords: {Address: [UInt64]}
87 access(account) var paymentsRecords: {Address: [UInt64]}
88
89 /// Reserved parameter fields: {ParamName: Value}
90 access(self) let _reservedFields: {String: AnyStruct}
91
92
93
94
95 /** ____ _ _ _ _ ____ ___ _ ____ _ _ ____ _ _ ___ _ _
96 * |___ | | |\ | | | | | | |\ | |__| | | | \_/
97 * | |__| | \| |___ | | |__| | \| | | |___ | | |
98 ***********************************************************/
99
100
101 pub resource interface IdentityCertificate {}
102
103 pub resource UserCertificate: IdentityCertificate{
104
105 }
106
107 pub resource Payment {
108 pub let id: UInt64
109 pub var desc: String
110 pub let creator: Address
111 pub let config: {String: AnyStruct}
112 pub var type: PaymentType
113 pub let vault: @FungibleToken.Vault
114 pub var ticket: @MelodyTicket.NFT?
115 pub var withdrawn: UFix64
116 pub var status: PaymentStatus
117
118 pub var metadata: {String: AnyStruct}
119
120 init(id: UInt64, desc: String, creator: Address, type: PaymentType, vault: @FungibleToken.Vault, config: {String: AnyStruct}) {
121 self.id = id
122 self.desc = desc
123 self.creator = creator
124 self.config = config
125 self.type = type
126 self.vault <- vault
127 self.ticket <- nil
128 self.withdrawn = 0.0
129 self.status = PaymentStatus.UPCOMING
130 self.metadata = {}
131 }
132
133 // query payment revocable
134 pub fun getRevocable (): Bool {
135 return self.type == Melody.PaymentType.REVOCABLE_STREAM || self.type == Melody.PaymentType.REVOCABLE_VESTING
136 }
137 // query balance
138 pub fun queryBalance(): UFix64 {
139 return self.vault.balance
140 }
141
142 // query metadata
143 pub fun getInfo(): {String: AnyStruct} {
144
145 let metadata: {String: AnyStruct} = {}
146 metadata["id"] = self.id
147 metadata["balance"] = self.vault.balance
148 metadata["withdrawn"] = self.withdrawn
149 metadata["claimable"] = self.getClaimable()
150 metadata["type"] = self.type.rawValue
151 metadata["status"] = self.status.rawValue
152 metadata["claimed"] = self.ticket == nil
153 metadata["creator"] = self.creator
154 metadata["desc"] = self.desc
155
156 let keys = self.config.keys
157 for key in keys {
158 metadata[key] = self.config[key]
159 }
160
161 let nftMetadata = MelodyTicket.getMetadata(self.id)!
162 if nftMetadata != nil {
163 metadata["recipient"] = (nftMetadata["owner"] as? Address)
164 }
165 metadata["ticketInfo"] = nftMetadata
166 return metadata
167 }
168
169 // get payment claimable amount
170 access(contract) fun getClaimable(): UFix64 {
171
172 let config = self.config
173 let withdrawn = self.withdrawn
174 let currentTimestamp = getCurrentBlock().timestamp
175 let startTimestamp = (self.config["startTimestamp"] as? UFix64?)!!
176 let vaultBalance = self.vault.balance
177 var claimable = 0.0
178
179 if self.status == PaymentStatus.COMPLETE || self.status == PaymentStatus.CANCELED {
180 return 0.0
181 }
182 if self.type == PaymentType.STREAM || self.type == PaymentType.REVOCABLE_STREAM {
183 let endTimestamp = (self.config["endTimestamp"] as? UFix64?)!!
184 let amount = (self.config["amount"] as? UFix64?)!!
185 // assert(1==2, message: "start time:".concat(startTimestamp.toString()).concat("end time:").concat(currentTimestamp.toString()))
186 var timeDelta = 0.0
187 if currentTimestamp <= startTimestamp {
188 return 0.0
189 }
190 if currentTimestamp > endTimestamp {
191 timeDelta = endTimestamp - startTimestamp
192 } else {
193 timeDelta = currentTimestamp - startTimestamp
194 }
195 let streamed = timeDelta / (endTimestamp - startTimestamp) * amount
196 claimable = streamed
197
198 } else {
199
200 let cliffDuration = (config["cliffDuration"] as? UFix64?)! ?? 0.0
201 let cliffAmount = (config["cliffAmount"] as? UFix64?)! ?? 0.0
202 let stepDuration = (config["stepDuration"] as? UFix64?)!!
203 let steps = (config["steps"] as? Int8?)!!
204 let stepAmount = (config["stepAmount"] as? UFix64?)!!
205 let timeAfterCliff = startTimestamp + cliffDuration
206
207 if currentTimestamp < timeAfterCliff {
208 return 0.0
209 }
210 var vested = cliffAmount
211
212 let passedSinceCliff = currentTimestamp - timeAfterCliff
213
214 var stepPassed = Int8(passedSinceCliff / stepDuration)
215 if stepPassed > steps {
216 stepPassed = steps
217 }
218
219 vested = vested + (UFix64(stepPassed) * stepAmount)
220 claimable = vested
221 }
222
223 return claimable
224 }
225
226
227
228 // === write funcs ===
229 // revoke payment
230 pub fun revokePayment(userCertificateCap: Capability<&{Melody.IdentityCertificate}>): @FungibleToken.Vault {
231 pre {
232 self.status != PaymentStatus.COMPLETE && self.status != PaymentStatus.CANCELED : MelodyError.errorEncode(msg: "Cannot cancel close payment", err: MelodyError.ErrorCode.WRONG_LIFE_CYCLE_STATE)
233 self.creator == userCertificateCap.borrow()!.owner!.address : MelodyError.errorEncode(msg: "Only owner can revokePayment", err: MelodyError.ErrorCode.ACCESS_DENIED)
234 self.type == PaymentType.REVOCABLE_STREAM || self.type == Melody.PaymentType.REVOCABLE_VESTING : MelodyError.errorEncode(msg: "Only revocable payment can be revoked", err: MelodyError.ErrorCode.PAYMENT_NOT_REVOKABLE)
235 }
236
237 let balance = self.vault.balance
238 self.status = PaymentStatus.CANCELED
239 Melody.updateTicketMetadata(id: self.id, key: "status", value: PaymentStatus.CANCELED.rawValue)
240
241 emit PaymentRevoked(paymentId: self.id, amount: balance, operator: self.creator)
242
243 return <- self.vault.withdraw(amount: balance)
244 }
245
246 // cache ticket while receiver do not have receievr resource
247 access(contract) fun chacheTicket(ticket: @MelodyTicket.NFT) {
248 pre {
249 self.ticket == nil : MelodyError.errorEncode(msg: "Ticket already cached", err: MelodyError.ErrorCode.ALREADY_EXIST)
250 }
251
252 let receievr = (self.config["receiver"] as? Address?)!
253
254 emit TicketCached(paymentId: self.id, ticketId: ticket.id, receiver: receievr!)
255
256 self.ticket <-! ticket
257 }
258
259 // claim cached ticket
260 access(contract) fun claimTicket(): @MelodyTicket.NFT {
261 pre {
262 self.ticket != nil : MelodyError.errorEncode(msg: "Ticket already cached", err: MelodyError.ErrorCode.ALREADY_EXIST)
263 }
264 let ticket <- self.ticket <- nil
265 self.config.remove(key: "receiver")
266
267 return <- ticket!
268 }
269
270 // update payment config
271 access(contract) fun updateConfig(_ key: String, value: AnyStruct) {
272 pre {
273 self.config[key] != nil : MelodyError.errorEncode(msg: "Not set vaule", err: MelodyError.ErrorCode.NOT_EXIST)
274 }
275 let oldVal = self.config[key]
276 self.config[key] = value
277
278 emit PaymentConfigUpdated(paymentId: self.id, key: key)
279
280 }
281
282 // change the payment to non-revocable
283 access(contract) fun changeRevocable() {
284 pre {
285 self.status != PaymentStatus.COMPLETE && self.status != PaymentStatus.CANCELED : MelodyError.errorEncode(msg: "Cannot change close payment ", err: MelodyError.ErrorCode.WRONG_LIFE_CYCLE_STATE)
286 }
287 let oldType = self.type
288 var type = oldType
289 if oldType == PaymentType.REVOCABLE_STREAM {
290 type = PaymentType.STREAM
291 } else if oldType == PaymentType.REVOCABLE_VESTING {
292 type = PaymentType.VESTING
293 }
294 assert(oldType != type, message: MelodyError.errorEncode(msg: "Canot set same type", err: MelodyError.ErrorCode.INVALID_PARAMETERS))
295 self.type = type
296 Melody.updateTicketMetadata(id: self.id, key: "paymentInfo", value: self.config)
297 Melody.updateTicketMetadata(id: self.id, key: "paymentType", value: type.rawValue)
298 emit PaymentTypeChanged(paymentId: self.id, oldType: oldType.rawValue, newType: type.rawValue)
299 }
300
301
302 // withdraw from payment
303 access(contract) fun withdraw(_ amount: UFix64): @FungibleToken.Vault {
304 pre {
305 self.status != PaymentStatus.COMPLETE && self.status != PaymentStatus.CANCELED : MelodyError.errorEncode(msg: "Cannot update close payment", err: MelodyError.ErrorCode.WRONG_LIFE_CYCLE_STATE)
306 }
307 self.withdrawn = self.withdrawn + amount
308 self.updateStatus()
309 return <- self.vault.withdraw(amount: amount)
310 }
311
312 // update payment status
313 access(self) fun updateStatus() {
314
315 let currentTimestamp = getCurrentBlock().timestamp
316 let startTimestamp = (self.config["startTimestamp"] as? UFix64?)!!
317
318 let oldStatus = self.status
319 var status = oldStatus
320 // update stream status
321 if self.type == PaymentType.STREAM || self.type == PaymentType.REVOCABLE_STREAM {
322 let endTimestamp = (self.config["endTimestamp"] as? UFix64?)!!
323
324 if self.status == PaymentStatus.UPCOMING && currentTimestamp >= startTimestamp {
325 status = PaymentStatus.ACTIVE
326 }
327 if self.status == PaymentStatus.ACTIVE && currentTimestamp >= endTimestamp {
328 status = PaymentStatus.COMPLETE
329 }
330 } else { // update vesing status
331 if self.status == PaymentStatus.UPCOMING && currentTimestamp >= startTimestamp {
332 status = PaymentStatus.ACTIVE
333 }
334 let stepDuration = (self.config["stepDuration"] as? UFix64?)!!
335 let steps = (self.config["steps"] as? Int8?)!!
336 let cliffDuration = (self.config["cliffDuration"] as? UFix64?)! ?? 0.0
337 let endVestingTimestamp = startTimestamp + cliffDuration + UFix64(steps) * stepDuration
338 if self.status == PaymentStatus.ACTIVE && currentTimestamp >= endVestingTimestamp {
339 status = PaymentStatus.COMPLETE
340 }
341 }
342
343 self.status = status
344 if status == PaymentStatus.COMPLETE {
345 Melody.updateTicketMetadata(id: self.id, key: "status", value: PaymentStatus.COMPLETE.rawValue)
346 }
347 if status == PaymentStatus.ACTIVE {
348 Melody.updateTicketMetadata(id: self.id, key: "status", value: PaymentStatus.ACTIVE.rawValue)
349 }
350
351 emit PaymentStatusUpdated(paymentId: self.id, oldStatus: oldStatus.rawValue, newStatus: status.rawValue)
352
353 }
354
355 destroy (){
356 pre {
357 self.status == PaymentStatus.COMPLETE && self.status == PaymentStatus.CANCELED : MelodyError.errorEncode(msg: "Cannot destroy active payment", err: MelodyError.ErrorCode.WRONG_LIFE_CYCLE_STATE)
358 self.vault.balance > 0.0 : MelodyError.errorEncode(msg: "Please withdraw the remaining funds", err: MelodyError.ErrorCode.WRONG_LIFE_CYCLE_STATE)
359 }
360
361 emit PaymentDestroyed(paymentId: self.id, ticketId: self.ticket?.id)
362
363 destroy self.vault
364 destroy self.ticket
365
366
367 }
368 }
369
370
371 // resources
372 // melody admin resource for manage melody contract
373 pub resource Admin {
374 // vaults for commission fee
375 access(self) var vaults: @{String: FungibleToken.Vault}
376
377 access(self) let payments: @{UInt64: Payment}
378
379 init() {
380 self.payments <- {}
381 self.vaults <- {}
382 }
383
384
385 pub fun setPause(_ flag: Bool) {
386 pre {
387 Melody.pause != flag : MelodyError.errorEncode(msg: "Set pause state faild, the state is same", err: MelodyError.ErrorCode.SAME_BOOL_STATE)
388 }
389 Melody.pause = flag
390
391 emit PauseStateChanged(pauseFlag: flag, operator: self.owner!.address)
392 }
393
394 pub fun setCommision(_ commision: UFix64) {
395
396 emit CommisionChanged(before: Melody.melodyCommision, after: commision)
397 Melody.melodyCommision = commision
398 }
399
400 pub fun setMinimumPayment(_ min: UFix64) {
401
402 emit MinimumPaymentChanged(before: Melody.minimumPayment, after: min)
403 Melody.minimumPayment = min
404 }
405
406 // minimal time duration in timpstamp
407 pub fun setGraceDuration(_ duration: UFix64) {
408 emit GraceDurationChanged(before: Melody.graceDuration, after: duration)
409 Melody.graceDuration = duration
410 }
411 // get payment ref
412 pub fun getPayment(_ id: UInt64): &Payment {
413 pre{
414 self.payments[id] != nil : MelodyError.errorEncode(msg: "Payment not found", err: MelodyError.ErrorCode.NOT_EXIST)
415 }
416 let paymentRef = (&self.payments[id] as &Payment?)!
417 return paymentRef
418 }
419
420 // save payment
421 pub fun savePayment(_ payment: @Payment) {
422 pre {
423 self.payments[payment.id] == nil : MelodyError.errorEncode(msg: "Payment already exists", err: MelodyError.ErrorCode.ALREADY_EXIST)
424 }
425 self.payments[payment.id] <-! payment
426 }
427
428 // deposite service fee
429 pub fun deposit(_ vault: @FungibleToken.Vault) {
430 let identifier = vault.getType().identifier
431 if self.vaults[identifier] == nil {
432 self.vaults[identifier] <-! vault
433 emit VaultCreated(identifier: identifier)
434 } else {
435 let vaultRef = (&self.vaults[identifier] as &FungibleToken.Vault?)!
436 emit VaultDeposited(identifier:identifier, amount: vault.balance)
437 vaultRef.deposit(from: <- vault)
438 }
439
440 }
441 // withdraw service fee
442 pub fun withdraw(_ key: String?, amount: UFix64?): @{String: FungibleToken.Vault} {
443 let vaults: @{String: FungibleToken.Vault} <- {}
444 var keys: [String] = []
445 if key != nil && key != "" {
446 let vaultRef = (&vaults[key!] as &FungibleToken.Vault?)!
447 let balance = vaultRef.balance
448 let withdrawAmount = amount ?? balance
449 vaults[key!] <-! vaultRef!.withdraw(amount: withdrawAmount)
450 emit VaultWithdrawn(identifier: key!, amount: withdrawAmount, balance: balance- withdrawAmount)
451 return <- vaults
452 } else {
453 keys = self.vaults.keys
454 for k in keys {
455 let vaultRef = (&vaults[k] as &FungibleToken.Vault?)!
456 let balance = vaultRef.balance
457 let withdrawAmount = amount ?? balance
458 vaults[k] <-! vaultRef!.withdraw(amount: withdrawAmount)
459 emit VaultWithdrawn(identifier: key!, amount: withdrawAmount, balance: balance- withdrawAmount)
460 }
461 return <- vaults
462 }
463 }
464
465 destroy() {
466 destroy self.payments
467 destroy self.vaults
468 }
469
470 }
471
472
473 // ---- contract funcs ----
474 // init user certificate
475 pub fun setupUser(): @UserCertificate {
476 let certificate <- create UserCertificate()
477 return <- certificate
478 }
479
480 // update ticket metadata
481 access(account) fun updateTicketMetadata(id: UInt64, key: String, value: AnyStruct) {
482 pre {
483 MelodyTicket.getMetadata(id) != nil : MelodyError.errorEncode(msg: "Ticket not found", err: MelodyError.ErrorCode.NOT_EXIST)
484 }
485 MelodyTicket.updateMetadata(id: id, key: key, value: value)
486 }
487
488 // set ticket metadata
489 access(account) fun setTicketMetadata(id: UInt64, metadata: {String: AnyStruct}) {
490 pre {
491 MelodyTicket.getMetadata(id) == nil : MelodyError.errorEncode(msg: "Ticket already exist", err: MelodyError.ErrorCode.ALREADY_EXIST)
492 }
493 MelodyTicket.setMetadata(id: id, metadata: metadata)
494 }
495
496 // update payments records
497 access(account) fun updatePaymentsRecords(address: Address, id: UInt64) {
498 let ids = self.paymentsRecords[address] ?? []
499 var newIds = ids
500 newIds.append(id)
501 self.paymentsRecords[address] = newIds
502
503 emit PaymentRecordsUpdated(address: address, before: ids, after: newIds)
504 }
505
506
507 // create stream
508 /**
509 ** @param userCertificateCap - creator cap to proof their identity
510 ** @param vault - contain the FT token to steam
511 ** @param receiver - the receiver address
512 ** @param revocable - stream can be revoke or not
513 ** @param config - config of create a stream
514 ** @param vaultIdentifier - the identifier of FT token's storagePath
515 ** @param startTimestamp - start time of stream
516 ** @param endTimestamp - end time of stream
517 ** @param transferable -
518 ** @param desc - desc of stream
519 */
520 pub fun createStream(userCertificateCap: Capability<&{Melody.IdentityCertificate}>, vault: @FungibleToken.Vault, receiver: Address, revocable: Bool, config: {String: AnyStruct}) {
521 pre {
522 vault.balance >= Melody.minimumPayment : MelodyError.errorEncode(msg: "Vault balance must be greater than ".concat(Melody.minimumPayment.toString()), err: MelodyError.ErrorCode.INVALID_PARAMETERS)
523 self.pause == false: MelodyError.errorEncode(msg: "Create stream is paused", err: MelodyError.ErrorCode.PAUSED)
524 }
525 let account = self.account
526 let adminRef = account.borrow<&Admin>(from: self.AdminStoragePath)!
527 let creator = userCertificateCap.borrow()!.owner!.address
528 let paymentId = Melody.totalCreated + UInt64(1)
529 Melody.streamCount = Melody.streamCount + UInt64(1)
530
531 let desc = (config["desc"] as? String) ?? ""
532 var type = PaymentType.STREAM
533 if revocable {
534 type = PaymentType.REVOCABLE_STREAM
535 }
536
537 let recipient = getAccount(receiver).getCapability<&{NonFungibleToken.CollectionPublic}>(MelodyTicket.CollectionPublicPath).borrow()
538
539
540 let balance = vault.balance
541 let currentTimestamp = getCurrentBlock().timestamp
542 let startTimestamp = (config["startTimestamp"] as? UFix64?)! ?? panic(MelodyError.errorEncode(msg: "Start timestamp invalid", err: MelodyError.ErrorCode.INVALID_PARAMETERS))
543 let endTimestamp = (config["endTimestamp"] as? UFix64?)! ?? panic(MelodyError.errorEncode(msg: "Endt timestamp invalid", err: MelodyError.ErrorCode.INVALID_PARAMETERS))
544 let transferable = (config["transferable"] as? Bool?)! ?? true
545
546 let vaultIdentifier = (config["vaultIdentifier"] as? String?)! ?? panic(MelodyError.errorEncode(msg: "Vault identifier invalid", err: MelodyError.ErrorCode.INVALID_PARAMETERS))
547 assert(vaultIdentifier != "", message: MelodyError.errorEncode(msg: "Must have vaultIdentifier", err: MelodyError.ErrorCode.INVALID_PARAMETERS))
548 assert(currentTimestamp + Melody.graceDuration <= startTimestamp, message: MelodyError.errorEncode(msg: "Start time must be greater than current time, currentTimestamp: ".concat(currentTimestamp.toString()).concat(" startTimestamp: ").concat(startTimestamp.toString()), err: MelodyError.ErrorCode.INVALID_PARAMETERS))
549 assert(endTimestamp > startTimestamp + Melody.graceDuration, message: MelodyError.errorEncode(msg: "End time must be greater than current time", err: MelodyError.ErrorCode.INVALID_PARAMETERS))
550
551 if recipient == nil {
552 config["receiver"] = receiver
553 }
554 config["amount"] = balance
555 config["vaultType"]= vault.getType().identifier
556
557 let payment <- create Payment(id: paymentId, desc:desc, creator: creator, type: type, vault: <- vault, config: config)
558
559 adminRef.savePayment(<- payment)
560 let paymentRef = adminRef.getPayment(paymentId)
561
562 self.totalCreated = paymentId
563 self.streamCount = self.streamCount + UInt64(1)
564 self.updatePaymentsRecords(address: creator, id: paymentId)
565
566 let ticketMinter = account.borrow<&MelodyTicket.NFTMinter>(from: MelodyTicket.MinterStoragePath)!
567
568
569 let name = "Melody".concat(" stream ticket#").concat(paymentId.toString())
570 // todo
571 let metadata: {String: AnyStruct} = {}
572 // metadata["paymentInfo"] = config
573 metadata["paymentType"] = type.rawValue
574 metadata["paymentId"] = paymentId
575 metadata["status"] = PaymentStatus.UPCOMING.rawValue
576 metadata["receiver"] = receiver
577 metadata["vaultType"]= paymentRef.vault.getType().identifier
578
579 if transferable == false {
580 metadata["transferable"] = false
581 }
582 // info for nft ticket
583 let nftMetadata: {String: AnyStruct} = {}
584 nftMetadata["creator"] = creator
585 nftMetadata["description"] = desc
586
587 emit PaymentCreated(paymentId: paymentRef.id, type: type.rawValue, creator: creator, receiver: receiver, amount: balance )
588
589 let nft <- ticketMinter.mintNFT(name: name, description: desc, metadata: nftMetadata)
590
591 self.setTicketMetadata(id: nft.id, metadata: metadata)
592
593 if recipient != nil {
594 recipient!.deposit(token: <- nft)
595 } else {
596 self.updateUserTicketsRecord(address: receiver, id: paymentRef.id, isDelete: false )
597 paymentRef.chacheTicket(ticket: <- nft)
598 }
599 }
600
601 // create vesting
602 /**
603 ** @param userCertificateCap - creator cap to proof their identity
604 ** @param vault - contain the FT token to vesting
605 ** @param receiver - the receiver address
606 ** @param revocable - vesting can be revoke or not
607 ** @param config - config of create a vesting
608 ** @param vaultIdentifier - the identifier of FT token's storagePath
609 ** @param startTimestamp - start time of vesting
610 ** @param cliffDuration - duration after startTimestamp to cliff
611 ** @param cliffAmount - amount to cliff
612 ** @param stepDuration - duration each steps
613 ** @param steps - steps of vesting
614 ** @param stepAmount - amount of vesting step
615 ** @param desc - desc of vesting
616 */
617 pub fun createVesting(userCertificateCap: Capability<&{Melody.IdentityCertificate}>, vault: @FungibleToken.Vault, receiver: Address, revocable: Bool, config: {String: AnyStruct}) {
618 pre {
619 vault.balance > Melody.minimumPayment : MelodyError.errorEncode(msg: "Vault balance must be greater than 0", err: MelodyError.ErrorCode.CAN_NOT_BE_ZERO)
620 self.pause == false: MelodyError.errorEncode(msg: "Create stream is paused", err: MelodyError.ErrorCode.PAUSED)
621 }
622 let account = self.account
623 let adminRef = account.borrow<&Admin>(from: self.AdminStoragePath)!
624 let creator = userCertificateCap.borrow()!.owner!.address
625 let paymentId = Melody.totalCreated + UInt64(1)
626 Melody.vestingCount = Melody.vestingCount + UInt64(1)
627
628 let desc = (config["desc"] as? String) ?? ""
629 var type = Melody.PaymentType.VESTING
630 if revocable {
631 type = PaymentType.REVOCABLE_VESTING
632 }
633
634 let recipient = getAccount(receiver).getCapability<&{NonFungibleToken.CollectionPublic}>(MelodyTicket.CollectionPublicPath).borrow()
635
636 // validate config
637 let balance = vault.balance
638 let currentTimestamp = getCurrentBlock().timestamp
639 let startTimestamp = (config["startTimestamp"] as? UFix64?)! ?? panic(MelodyError.errorEncode(msg: "Start timestamp invalid", err: MelodyError.ErrorCode.INVALID_PARAMETERS))
640 let cliffDuration = (config["cliffDuration"] as? UFix64?)! ?? 0.0
641 let cliffAmount = (config["cliffAmount"] as? UFix64?)! ?? 0.0
642 let stepDuration = (config["stepDuration"] as? UFix64?)! ?? panic(MelodyError.errorEncode(msg: "Step duration invalid", err: MelodyError.ErrorCode.INVALID_PARAMETERS))
643 let steps = (config["steps"] as? Int8?)! ?? panic(MelodyError.errorEncode(msg: "Step invalid", err: MelodyError.ErrorCode.INVALID_PARAMETERS))
644 let stepAmount = (config["stepAmount"] as? UFix64?)! ?? panic(MelodyError.errorEncode(msg: "Step amount invalid", err: MelodyError.ErrorCode.INVALID_PARAMETERS))
645 let transferable = (config["transferable"] as? Bool?)! ?? true
646 assert(steps >= 1, message: MelodyError.errorEncode(msg: "Step must greater than 0", err: MelodyError.ErrorCode.INVALID_PARAMETERS))
647
648 let totalAmount = cliffAmount + UFix64(steps) * stepAmount!
649 let vaultIdentifier = (config["vaultIdentifier"] as? String?)! ?? panic(MelodyError.errorEncode(msg: "Vault identifier invalid", err: MelodyError.ErrorCode.INVALID_PARAMETERS))
650
651 assert(stepAmount >= Melody.minimumPayment, message: MelodyError.errorEncode(msg: "Step amount must be greater than minimum payment", err: MelodyError.ErrorCode.INVALID_PARAMETERS))
652 assert(vaultIdentifier != "", message: MelodyError.errorEncode(msg: "Must have vaultIdentifier", err: MelodyError.ErrorCode.INVALID_PARAMETERS))
653 // assert(cliffAmount > 0.0 && cliffDuration + startTimestamp > currentTimestamp, message: MelodyError.errorEncode(msg: "Start time must be greater than current time", err: MelodyError.ErrorCode.INVALID_PARAMETERS))
654 if cliffAmount > 0.0 || cliffDuration > 0.0 {
655 assert(cliffAmount > 0.0 && cliffDuration > 0.0, message: MelodyError.errorEncode(msg: "Cliff amount and duration invalid", err: MelodyError.ErrorCode.INVALID_PARAMETERS))
656 assert(cliffDuration % 1.0 == 0.0, message: MelodyError.errorEncode(msg: "Cliff duration can not be a decimal ", err: MelodyError.ErrorCode.INVALID_PARAMETERS))
657 }
658 assert(balance >= totalAmount, message: MelodyError.errorEncode(msg: "Valut balance not enougth - balance: ".concat(balance.toString()).concat("required: ").concat(totalAmount.toString()), err: MelodyError.ErrorCode.INVALID_PARAMETERS))
659 assert(currentTimestamp + Melody.graceDuration <= startTimestamp, message: MelodyError.errorEncode(msg: "Start time must be greater than current time with grace period, currentTimestamp: ".concat(currentTimestamp.toString()).concat(" startTimestamp: ").concat(startTimestamp.toString()), err: MelodyError.ErrorCode.INVALID_PARAMETERS))
660 assert(stepDuration >= Melody.graceDuration, message: MelodyError.errorEncode(msg: "Step duration must be greater than grace period", err: MelodyError.ErrorCode.INVALID_PARAMETERS))
661 assert(stepDuration % 1.0 == 0.0, message: MelodyError.errorEncode(msg: "Step duration can not be a decimal ", err: MelodyError.ErrorCode.INVALID_PARAMETERS))
662
663 if recipient == nil {
664 config["receiver"] = receiver
665 }
666 config["amount"] = balance
667 config["vaultType"]= vault.getType().identifier
668
669 let payment <- create Payment(id: paymentId, desc:desc, creator: creator, type: type, vault: <- vault, config: config)
670
671 adminRef.savePayment(<- payment)
672 let paymentRef = adminRef.getPayment(paymentId)
673
674 self.totalCreated = paymentId
675 self.streamCount = self.streamCount + UInt64(1)
676 self.updatePaymentsRecords(address: creator, id: paymentId)
677
678 let ticketMinter = account.borrow<&MelodyTicket.NFTMinter>(from: MelodyTicket.MinterStoragePath)!
679
680 let name = "Melody".concat(" vesting ticket#").concat(paymentId.toString())
681
682 let metadata:{String: AnyStruct} = {}
683 metadata["paymentInfo"] = config
684 metadata["paymentType"] = type.rawValue
685 metadata["paymentId"] = paymentId
686 metadata["status"] = PaymentStatus.UPCOMING.rawValue
687 metadata["receiver"] = receiver
688
689
690 if transferable == false {
691 metadata["transferable"] = false
692 }
693 // info for nft ticket
694 let nftMetadata: {String: AnyStruct} = {}
695 nftMetadata["creator"] = creator
696 nftMetadata["description"] = desc
697
698 emit PaymentCreated(paymentId: paymentRef.id, type: type.rawValue, creator: creator, receiver: receiver, amount: balance )
699 let nft <- ticketMinter.mintNFT(name: name, description: desc, metadata: nftMetadata)
700
701 self.setTicketMetadata(id: nft.id, metadata: metadata)
702
703 // cache the ticket nft while the receiver dont have resource
704 if recipient != nil {
705 recipient!.deposit(token: <- nft)
706 } else {
707 paymentRef.chacheTicket(ticket: <- nft)
708 self.updateUserTicketsRecord(address: receiver, id:paymentRef.id, isDelete: false )
709 }
710 }
711
712 // change payment revocable to non-revocable
713 pub fun revokePayment(userCertificateCap: Capability<&{Melody.IdentityCertificate}>, paymentId: UInt64): @FungibleToken.Vault {
714 pre {
715 self.paymentsRecords[userCertificateCap.borrow()!.owner!.address]!.contains(paymentId): MelodyError.errorEncode(msg: "Access denied when cancle payment", err: MelodyError.ErrorCode.ACCESS_DENIED)
716
717 }
718 let paymentRef = self.account.borrow<&Admin>(from: self.AdminStoragePath)!.getPayment(paymentId)
719
720 assert(paymentRef.status != PaymentStatus.CANCELED || paymentRef.status != PaymentStatus.COMPLETE , message: MelodyError.errorEncode(msg: "Payment already canceled", err: MelodyError.ErrorCode.WRONG_LIFE_CYCLE_STATE))
721 assert(paymentRef.type != PaymentType.VESTING && paymentRef.type != PaymentType.STREAM, message: MelodyError.errorEncode(msg: "Cannot cancel non-revoked payment", err: MelodyError.ErrorCode.WRONG_LIFE_CYCLE_STATE))
722 return <- paymentRef.revokePayment(userCertificateCap: userCertificateCap)
723 }
724
725 // change payment revocable to non-revocable
726 pub fun changeRevocable(userCertificateCap: Capability<&{Melody.IdentityCertificate}>, paymentId: UInt64) {
727 pre {
728 self.paymentsRecords[userCertificateCap.borrow()!.owner!.address]!.contains(paymentId): MelodyError.errorEncode(msg: "Access denied when update payment info", err: MelodyError.ErrorCode.ACCESS_DENIED)
729 }
730 let paymentRef = self.account.borrow<&Admin>(from: self.AdminStoragePath)!.getPayment(paymentId)
731
732 assert(paymentRef.status != PaymentStatus.CANCELED || paymentRef.status != PaymentStatus.COMPLETE , message: MelodyError.errorEncode(msg: "Cannot change revocable with canceled payment", err: MelodyError.ErrorCode.WRONG_LIFE_CYCLE_STATE))
733 assert(paymentRef.type != PaymentType.VESTING && paymentRef.type != PaymentType.STREAM, message: MelodyError.errorEncode(msg: "Cannot change revocable with non-revoked payment", err: MelodyError.ErrorCode.WRONG_LIFE_CYCLE_STATE))
734 paymentRef.changeRevocable()
735 }
736
737
738 // change payment ticket transferable if is non-transferable
739 pub fun changeTransferable(userCertificateCap: Capability<&{Melody.IdentityCertificate}>, paymentId: UInt64) {
740 pre {
741 self.paymentsRecords[userCertificateCap.borrow()!.owner!.address]!.contains(paymentId): MelodyError.errorEncode(msg: "Access denied when update payment info", err: MelodyError.ErrorCode.ACCESS_DENIED)
742 }
743 let paymentRef = self.account.borrow<&Admin>(from: self.AdminStoragePath)!.getPayment(paymentId)
744 let transferable = (paymentRef.config["transferable"] as? Bool?)! ?? true
745 assert(paymentRef.status != PaymentStatus.CANCELED || paymentRef.status != PaymentStatus.COMPLETE , message: MelodyError.errorEncode(msg: "Cannot change transferable with canceled payment", err: MelodyError.ErrorCode.WRONG_LIFE_CYCLE_STATE))
746 assert(transferable == false, message: MelodyError.errorEncode(msg: "Only allow non-transferable to transferable", err: MelodyError.ErrorCode.WRONG_LIFE_CYCLE_STATE))
747 paymentRef.updateConfig("transferable", value: true)
748 let nftMetadata = MelodyTicket.getMetadata(paymentId)
749 Melody.updateTicketMetadata(id: paymentId, key: "paymentInfo", value: paymentRef.config)
750 Melody.updateTicketMetadata(id: paymentId, key: "transferable", value: true)
751
752 }
753
754 // claim cached ticket
755 pub fun claimTicket(userCertificateCap: Capability<&{Melody.IdentityCertificate}>, paymentId: UInt64): @MelodyTicket.NFT {
756 pre {
757 self.getUserTicketRecords(userCertificateCap.borrow()!.owner!.address)!.contains(paymentId): MelodyError.errorEncode(msg: "Access denied when claim ticket", err: MelodyError.ErrorCode.ACCESS_DENIED)
758 }
759 let paymentRef = self.account.borrow<&Admin>(from: self.AdminStoragePath)!.getPayment(paymentId)
760 assert(paymentRef.status != PaymentStatus.CANCELED, message: MelodyError.errorEncode(msg: "Cannot claim ticket from canceled payment", err: MelodyError.ErrorCode.WRONG_LIFE_CYCLE_STATE))
761 let config = paymentRef.config
762 let recipient = (config["receiver"] as? Address?)!
763 assert(recipient == userCertificateCap.borrow()!.owner!.address, message: MelodyError.errorEncode(msg: "Cannot claim ticket from wrong receiver", err: MelodyError.ErrorCode.ACCESS_DENIED))
764
765
766 self.updateUserTicketsRecord(address: recipient!, id:paymentRef.id, isDelete: true )
767 let ticketRef = &paymentRef.ticket as &MelodyTicket.NFT?
768
769 emit TicketClaimed(paymentId: paymentRef.id, ticketId: ticketRef!.id, receiver: recipient! )
770
771 return <- paymentRef.claimTicket()
772 }
773
774 // withdraw funds from payment
775 pub fun withdraw(userCertificateCap: Capability<&{Melody.IdentityCertificate}>, ticket: &MelodyTicket.NFT): @FungibleToken.Vault {
776 pre {
777 userCertificateCap.borrow()!.owner!.address == ticket.owner!.address : MelodyError.errorEncode(msg: "Withdraw ", err: MelodyError.ErrorCode.ACCESS_DENIED)
778 }
779 // todo
780 let paymentRef = self.account.borrow<&Admin>(from: self.AdminStoragePath)!.getPayment(ticket.id)
781
782 var vault: @FungibleToken.Vault? <- nil
783
784 if paymentRef.type == PaymentType.VESTING || paymentRef.type == PaymentType.REVOCABLE_VESTING {
785 vault <-! self.withdrawVesting(ticket: ticket)
786 } else {
787 vault <-! self.withdrawStream(ticket: ticket)
788 }
789
790 return <- vault!
791 }
792
793 // stream payment withdraw
794 access(contract) fun withdrawStream(ticket: &MelodyTicket.NFT): @FungibleToken.Vault {
795
796 let paymentRef = self.account.borrow<&Admin>(from: self.AdminStoragePath)!.getPayment(ticket.id)
797 let paymentType = paymentRef.type
798 let paymentStatus = paymentRef.status
799 let config = paymentRef.config
800 let vaultRef = &paymentRef.vault as! &FungibleToken.Vault
801 let withdrawn = paymentRef.withdrawn
802
803 assert(paymentType == PaymentType.STREAM || paymentType == PaymentType.REVOCABLE_STREAM, message: MelodyError.errorEncode(msg: "Can only withdraw from stream payment", err: MelodyError.ErrorCode.TYPE_MISMATCH))
804 assert(paymentStatus == PaymentStatus.UPCOMING || paymentStatus == PaymentStatus.ACTIVE, message: MelodyError.errorEncode(msg: "Can withdraw with wrong status", err: MelodyError.ErrorCode.WRONG_LIFE_CYCLE_STATE))
805
806 let currentTimestamp = getCurrentBlock().timestamp
807
808 let startTimestamp = (config["startTimestamp"] as? UFix64?)!!
809 let vaultBalance = vaultRef.balance
810
811 assert(currentTimestamp > startTimestamp, message: MelodyError.errorEncode(msg: "Can not withdraw before start", err: MelodyError.ErrorCode.WRONG_LIFE_CYCLE_STATE))
812 let streamed = paymentRef.getClaimable()
813 let canClaimAmount = streamed - withdrawn
814
815 assert(streamed >= withdrawn, message: MelodyError.errorEncode(msg: "Steamed amount must greater than withdraw amount", err: MelodyError.ErrorCode.WRONG_LIFE_CYCLE_STATE))
816 assert(canClaimAmount <= vaultBalance, message: MelodyError.errorEncode(msg: "Withdraw amount must lower than vault balance", err: MelodyError.ErrorCode.WRONG_LIFE_CYCLE_STATE))
817 let withdrawVault <- paymentRef.withdraw(canClaimAmount)
818
819 // commision cut
820 self.cutCommision(&withdrawVault as &FungibleToken.Vault, paymentId: paymentRef.id)
821
822 emit PaymentWithdrawn(paymentId: paymentRef.id, type: paymentType.rawValue, status: paymentStatus.rawValue, amount: canClaimAmount)
823
824 return <- withdrawVault
825 }
826
827 // vesting payment withdraw
828 access(contract) fun withdrawVesting(ticket: &MelodyTicket.NFT): @FungibleToken.Vault {
829 let paymentRef = self.account.borrow<&Admin>(from: self.AdminStoragePath)!.getPayment(ticket.id)
830 let paymentType = paymentRef.type
831 let paymentStatus = paymentRef.status
832 let config = paymentRef.config
833 let vaultRef = &paymentRef.vault as! &FungibleToken.Vault
834 let withdrawn = paymentRef.withdrawn
835
836 assert(paymentType == PaymentType.VESTING || paymentType == PaymentType.REVOCABLE_VESTING, message: MelodyError.errorEncode(msg: "Can only withdraw from vesting payment", err: MelodyError.ErrorCode.TYPE_MISMATCH))
837 assert(paymentStatus == PaymentStatus.UPCOMING || paymentStatus == PaymentStatus.ACTIVE, message: MelodyError.errorEncode(msg: "Can withdraw with wrong status", err: MelodyError.ErrorCode.WRONG_LIFE_CYCLE_STATE))
838
839 let currentTimestamp = getCurrentBlock().timestamp
840 let startTimestamp = (config["startTimestamp"] as? UFix64?)!!
841 let vaultBalance = vaultRef.balance
842
843 assert(currentTimestamp >= startTimestamp, message: MelodyError.errorEncode(msg: "Can withdraw before start".concat(currentTimestamp.toString()).concat(startTimestamp.toString()), err: MelodyError.ErrorCode.WRONG_LIFE_CYCLE_STATE))
844 let claimable = paymentRef.getClaimable()
845 let canClaimAmount = claimable - withdrawn
846 assert(canClaimAmount <= vaultBalance, message: MelodyError.errorEncode(msg: "Withdraw amount must lower than vault balance", err: MelodyError.ErrorCode.WRONG_LIFE_CYCLE_STATE))
847 assert(claimable >= withdrawn, message: MelodyError.errorEncode(msg: "Vesting claimable amount must greater than withdraw amount", err: MelodyError.ErrorCode.WRONG_LIFE_CYCLE_STATE))
848 assert(canClaimAmount > 0.0, message: MelodyError.errorEncode(msg: "No amount can claim", err: MelodyError.ErrorCode.CAN_NOT_BE_ZERO))
849
850 let withdrawVault <- paymentRef.withdraw(canClaimAmount)
851
852 self.cutCommision(&withdrawVault as &FungibleToken.Vault, paymentId: paymentRef.id)
853
854 return <- withdrawVault
855 }
856
857 // cut commision at withdraw
858 access(contract) fun cutCommision(_ vaultRef: &FungibleToken.Vault, paymentId: UInt64){
859 if self.melodyCommision > 0.0 {
860 let adminRef = self.account.borrow<&Admin>(from: self.AdminStoragePath)!
861 let commisionAmount = vaultRef.balance * self.melodyCommision
862 emit CommisionSended(paymentId: paymentId, identifier: vaultRef.getType().identifier, amount: commisionAmount)
863 adminRef.deposit(<- vaultRef.withdraw(amount: commisionAmount))
864
865 }
866 }
867
868 // update ticket record
869 access(contract) fun updateUserTicketsRecord(address: Address, id: UInt64, isDelete: Bool){
870 if isDelete == true && Melody.userTicketRecords[address]!.contains(id) == false {
871 panic(MelodyError.errorEncode(msg: "Delete failed: record not existed", err: MelodyError.ErrorCode.INVALID_PARAMETERS))
872 }
873
874 let userTicketRecords = Melody.userTicketRecords[address] ?? []
875 var newRecords = userTicketRecords
876 if isDelete {
877 let index = newRecords.firstIndex(of: id)!
878 newRecords.remove(at: index)
879
880 } else {
881 newRecords.append(id)
882 }
883
884 Melody.userTicketRecords[address] = newRecords
885 emit TicketRecordChanged(address: address , before: userTicketRecords, after: newRecords)
886 }
887
888 // query creator's payment ids
889 pub fun getPaymentsIdRecords(_ address: Address): [UInt64] {
890 let ids = self.paymentsRecords[address] ?? []
891 return ids
892 }
893
894 // query payment info
895 pub fun getPaymentInfo(_ id: UInt64): {String: AnyStruct} {
896 var info: {String: AnyStruct} = {}
897 let paymentRef = self.account.borrow<&Admin>(from: self.AdminStoragePath)!.getPayment(id)
898
899 info = paymentRef.getInfo()
900
901 return info
902 }
903
904 // get user unclaim ticket records
905 pub fun getUserTicketRecords(_ address: Address): [UInt64] {
906 let ids = self.userTicketRecords[address] ?? []
907 return ids
908 }
909
910
911
912 // ---- init func ----
913 init() {
914 self.UserCertificateStoragePath = /storage/melodyUserCertificate
915 self.UserCertificatePrivatePath = /private/melodyUserCertificate
916 self._reservedFields = {}
917 self.totalCreated = 0
918 self.vestingCount = 0
919 self.streamCount = 0
920 self.pause = false
921
922 // for store the unclaim ticket for users
923 self.userTicketRecords = {}
924 self.paymentsRecords = {}
925
926 self.melodyCommision = 0.01
927
928 self.minimumPayment = 0.1
929
930 self.graceDuration = 300.0
931
932 self.AdminStoragePath = /storage/MelodyAdmin
933
934 let account = self.account
935 let admin <- create Admin()
936 account.save(<- admin, to: self.AdminStoragePath)
937
938 account.save(<- create UserCertificate(), to: self.UserCertificateStoragePath)
939 }
940
941}
942