Smart Contract

Melody

A.32a6af84f2f54476.Melody

Deployed

2h ago
Feb 28, 2026, 11:36:14 PM UTC

Dependents

0 imports
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