diff --git a/contracts/RaffleSources.cdc b/contracts/RaffleSources.cdc new file mode 100644 index 0000000..6db27b3 --- /dev/null +++ b/contracts/RaffleSources.cdc @@ -0,0 +1,49 @@ +import "Raffles" + +pub contract RaffleSources { + pub resource GenericRaffleSource: Raffles.RaffleSource { + pub let entries: [AnyStruct] + pub let entryType: Type + + pub fun getEntryType(): Type { + return self.entryType + } + + pub fun getEntryAt(index: Int): AnyStruct { + return self.entries[index] + } + + pub fun addEntry(_ v: AnyStruct) { + pre { + v.getType() == self.entryType: "incorrect entry type" + } + + self.entries.append(v) + } + + pub fun addEntries(_ v: [AnyStruct]) { + pre { + VariableSizedArrayType(self.entryType) == v.getType(): "incorrect array type" + } + + self.entries.appendAll(v) + } + + pub fun getNumEntries(): Int { + return self.entries.length + } + + pub fun getEntries(): [AnyStruct] { + return self.entries + } + + init(_ entryType: Type) { + self.entries = [] + self.entryType = entryType + } + } + + pub fun createRaffleSource(_ type: Type): @GenericRaffleSource { + return <- create GenericRaffleSource(type) + } +} \ No newline at end of file diff --git a/contracts/Raffles.cdc b/contracts/Raffles.cdc index d47cce3..d7f3eb7 100644 --- a/contracts/Raffles.cdc +++ b/contracts/Raffles.cdc @@ -4,8 +4,8 @@ pub contract Raffles { pub let ManagerStoragePath: StoragePath pub let ManagerPublicPath: PublicPath - pub event RaffleCreated(address: Address, raffleID: UInt64, sourceType: Type) - pub event RaffleDrawn(address: Address, raffleID: UInt64, sourceType: Type, index: Int, value: String) + pub event RaffleCreated(address: Address?, raffleID: UInt64, sourceType: Type) + pub event RaffleDrawn(address: Address?, raffleID: UInt64, sourceType: Type, index: Int, value: String, valueType: Type) pub struct Details { pub let start: UInt64? @@ -14,7 +14,7 @@ pub contract Raffles { init( _ start: UInt64?, - _ end: UInt64, + _ end: UInt64?, _ display: MetadataViews.Display ) { @@ -24,22 +24,70 @@ pub contract Raffles { } } + pub struct DrawingSelection { + pub let index: Int + pub let value: AnyStruct + + init(_ index: Int, _ value: AnyStruct) { + self.index = index + self.value = value + } + } + pub resource interface RafflePublic { pub fun getEntryAt(index: Int): AnyStruct + pub fun getDetails(): Details + pub fun getNumEntries(): Int + pub fun getEntries(): [AnyStruct] + pub fun draw(): DrawingSelection + } + + pub resource interface RaffleSource { + pub fun getEntryAt(index: Int): AnyStruct + pub fun addEntry(_ v: AnyStruct) + pub fun addEntries(_ v: [AnyStruct]) + pub fun getNumEntries(): Int + pub fun getEntries(): [AnyStruct] } pub resource Raffle: RafflePublic, MetadataViews.Resolver { pub let source: @{RaffleSource} pub let details: Details - pub fun draw(): Int { - return self.source.draw() + pub fun getDetails(): Details { + return self.details + } + + pub fun getNumEntries(): Int { + return self.source.getNumEntries() + } + + pub fun getEntries(): [AnyStruct] { + return self.source.getEntries() + } + + pub fun draw(): DrawingSelection { + let numEntries = self.source.getNumEntries() + let r = revertibleRandom() + let index = Int(r % UInt64(numEntries)) + let value = self.source.getEntryAt(index: index) + + Raffles.emitDrawing(self.owner?.address, self.uuid, self.source.getType(), index, value) + return DrawingSelection(index, value) } pub fun getEntryAt(index: Int): AnyStruct { return self.source.getEntryAt(index: index) } + pub fun addEntry(_ v: AnyStruct) { + self.source.addEntry(v) + } + + pub fun addEntries(_ v: [AnyStruct]) { + self.source.addEntries(v) + } + pub fun getViews(): [Type] { return [ Type() @@ -68,28 +116,6 @@ pub contract Raffles { } } - pub resource interface RaffleSource { - pub fun draw(): Int - pub fun getEntryAt(index: Int): AnyStruct - } - - pub resource AddressRaffleSource: RaffleSource { - pub let addresses: [Address] - - pub fun draw(): Int { - let r = revertibleRandom() - return Int(r % UInt64(self.addresses.length)) - } - - pub fun getEntryAt(index: Int): AnyStruct { - return self.addresses[index] - } - - init() { - self.addresses = [] - } - } - pub resource interface ManagerPublic { pub fun borrowRafflePublic(id: UInt64): &{RafflePublic}? } @@ -97,7 +123,23 @@ pub contract Raffles { pub resource Manager: ManagerPublic { access(self) let raffles: @{UInt64: Raffle} + pub fun createRaffle(source: @{RaffleSource}, details: Details): UInt64 { + let sourceType = source.getType() + + let raffle <- create Raffle(source: <- source, details: details) + emit RaffleCreated(address: self.owner!.address, raffleID: raffle.uuid, sourceType: sourceType) + + let id = raffle.uuid + destroy self.raffles.insert(key: id, <-raffle) + + return id + } + pub fun borrowRafflePublic(id: UInt64): &{RafflePublic}? { + return self.borrowRaffle(id: id) + } + + pub fun borrowRaffle(id: UInt64): &Raffle? { if self.raffles[id] == nil { return nil } @@ -114,7 +156,7 @@ pub contract Raffles { } } - access(contract) fun emitDrawing(address: Address, raffleID: UInt64, sourceType: Type, index: Int, value: AnyStruct) { + access(contract) fun emitDrawing(_ address: Address?, _ raffleID: UInt64, _ sourceType: Type, _ index: Int, _ value: AnyStruct) { var v = "UNKNOWN" switch value.getType() { case Type
(): @@ -125,26 +167,13 @@ pub contract Raffles { break } - emit RaffleDrawn(address: address, raffleID: raffleID, sourceType: sourceType, index: index, value: v) + emit RaffleDrawn(address: address, raffleID: raffleID, sourceType: sourceType, index: index, value: v, valueType: value.getType()) } pub fun createManager(): @Manager { return <- create Manager() } - pub fun createRaffle(source: @{RaffleSource}, details: Details): @Raffle { - return <- create Raffle(source: <- source, details: details) - } - - pub fun createRaffleSource(_ type: Type): @{RaffleSource} { - switch type { - case Type<@AddressRaffleSource>(): - return <- create AddressRaffleSource() - } - - panic("raffle source type ".concat(type.identifier).concat(" is not valid")) - } - init() { let identifier = "Raffle_".concat(self.account.address.toString()) self.ManagerStoragePath = StoragePath(identifier: identifier)! diff --git a/flow.json b/flow.json index d993d07..24d7a7f 100644 --- a/flow.json +++ b/flow.json @@ -26,6 +26,12 @@ "testing": "0x0000000000000007" } }, + "RaffleSources": { + "source": "./contracts/RaffleSources.cdc", + "aliases": { + "testing": "0x0000000000000008" + } + }, "FungibleToken": { "source": "./node_modules/@flowtyio/flow-contracts/contracts/FungibleToken.cdc", "aliases": { diff --git a/scripts/get_num_raffle_entries.cdc b/scripts/get_num_raffle_entries.cdc new file mode 100644 index 0000000..f62f129 --- /dev/null +++ b/scripts/get_num_raffle_entries.cdc @@ -0,0 +1,11 @@ +import "Raffles" + +pub fun main(addr: Address, id: UInt64): Int { + let acct = getAuthAccount(addr) + let manager = acct.borrow<&Raffles.Manager{Raffles.ManagerPublic}>(from: Raffles.ManagerStoragePath) + ?? panic("raffles manager not found") + let raffle = manager.borrowRafflePublic(id: id) + ?? panic("raffle not found") + + return raffle.getNumEntries() +} \ No newline at end of file diff --git a/scripts/get_raffle_details.cdc b/scripts/get_raffle_details.cdc new file mode 100644 index 0000000..6a828a5 --- /dev/null +++ b/scripts/get_raffle_details.cdc @@ -0,0 +1,13 @@ +import "Raffles" + +pub fun main(addr: Address, id: UInt64): Raffles.Details? { + let acct = getAuthAccount(addr) + let manager = acct.borrow<&Raffles.Manager>(from: Raffles.ManagerStoragePath) + ?? panic("raffles manager not found") + + if let raffle = manager.borrowRafflePublic(id: id) { + return raffle.getDetails() + } + + return nil +} \ No newline at end of file diff --git a/scripts/get_raffle_entries.cdc b/scripts/get_raffle_entries.cdc new file mode 100644 index 0000000..4f3218a --- /dev/null +++ b/scripts/get_raffle_entries.cdc @@ -0,0 +1,11 @@ +import "Raffles" + +pub fun main(addr: Address, id: UInt64): [AnyStruct] { + let acct = getAuthAccount(addr) + let manager = acct.borrow<&Raffles.Manager{Raffles.ManagerPublic}>(from: Raffles.ManagerStoragePath) + ?? panic("raffles manager not found") + let raffle = manager.borrowRafflePublic(id: id) + ?? panic("raffle not found") + + return raffle.getEntries() +} \ No newline at end of file diff --git a/scripts/get_raffle_source_identifier.cdc b/scripts/get_raffle_source_identifier.cdc new file mode 100644 index 0000000..ea4f349 --- /dev/null +++ b/scripts/get_raffle_source_identifier.cdc @@ -0,0 +1,7 @@ +import "Raffles" + +pub fun main(addr: Address, path: StoragePath): String { + let acct = getAuthAccount(addr) + let source = acct.borrow<&{Raffles.RaffleSource}>(from: path) + return source!.getType().identifier +} \ No newline at end of file diff --git a/test/Raffles_tests.cdc b/test/Raffles_tests.cdc index baf3a55..32eaa13 100644 --- a/test/Raffles_tests.cdc +++ b/test/Raffles_tests.cdc @@ -2,15 +2,141 @@ import Test import "test_helpers.cdc" import "Raffles" +import "RaffleSources" +import "MetadataViews" +pub let RafflesContractAddress = Address(0x0000000000000007) +pub let RaffleSourcesContractAddress = Address(0x0000000000000008) + +pub let GenericRaffleSourceIdentifier = "A.0000000000000008.RaffleSources.GenericRaffleSource" pub fun setup() { - let err = Test.deployContract(name: "Raffles", path: "../contracts/Raffles.cdc", arguments: []) + var err = Test.deployContract(name: "Raffles", path: "../contracts/Raffles.cdc", arguments: []) + Test.expect(err, Test.beNil()) + + err = Test.deployContract(name: "RaffleSources", path: "../contracts/RaffleSources.cdc", arguments: []) Test.expect(err, Test.beNil()) } -pub fun setupManager() { +pub fun testSetupManager() { let acct = Test.createAccount() - txExecutor("setup_manager", [acct], [], nil) + txExecutor("setup_manager.cdc", [acct], [], nil) scriptExecutor("borrow_manager.cdc", [acct.address]) +} + +pub fun testCreateRaffleSource() { + let acct = Test.createAccount() + let path = StoragePath(identifier: "testCreateRaffleSource")! + txExecutor("create_raffle_source.cdc", [acct], [Type
(), path], nil) + let res = scriptExecutor("get_raffle_source_identifier.cdc", [acct.address, path]) + assert(res! as! String == GenericRaffleSourceIdentifier, message: "unexpected raffle source identifier") +} + +pub fun testCreateRaffle() { + let acct = Test.createAccount() + let name = "testCreateRaffle" + let description = "testCreateRaffle desc" + let thumbnail = "https://example.com/thumbnail" + let start: UInt64? = nil + let end: UInt64? = nil + + txExecutor("create_raffle.cdc", [acct], [Type
(), start, end, name, description, thumbnail], nil) + let createEvent = (Test.eventsOfType(Type()).removeLast() as! Raffles.RaffleCreated) + assert(acct.address == createEvent.address) + + let details = getRaffleDetails(acct, createEvent.raffleID) ?? panic("raffle not found") + + assert(name == details.display.name) + assert(description == details.display.description) + assert(thumbnail == details.display.thumbnail.uri()) + assert(start == details.start) + assert(end == details.end) +} + +pub fun testAddToRaffle() { + let acct = Test.createAccount() + let id = createAddressRaffle(acct) + let beforeEntries = getRaffleEntries(acct, id)! + assert(beforeEntries.length == 0) + + addEntryToRaffle(acct, id, acct.address) + + + let afterEntries = getRaffleEntries(acct, id)! + assert(afterEntries.length == 1) + assert(afterEntries[0] as! Address == acct.address) +} + +pub fun testDrawFromRaffle() { + let acct = Test.createAccount() + let id = createAddressRaffle(acct) + let beforeEntries = getRaffleEntries(acct, id)! + addEntryToRaffle(acct, id, acct.address) + + // make sure we can draw an entry from the raffle, even when it only has a single item in it + let drawing = drawFromRaffle(acct, acct.address, id) + assert(drawing == acct.address.toString()) + + // now let's add lots of additional entries + let accounts: {String: Test.Account} = { + acct.address.toString(): acct + } + var count = 0 + while count < 10 { + count = count + 1 + let a = Test.createAccount() + accounts[a.address.toString()] = a + } + + // draw again, making sure that the winner is in our dictionary + let drawing2 = drawFromRaffle(acct, acct.address, id) + assert(accounts[drawing2] != nil) +} + +pub fun getRaffleDetails(_ acct: Test.Account, _ id: UInt64): Raffles.Details? { + if let res = scriptExecutor("get_raffle_details.cdc", [acct.address, id]) { + return res as! Raffles.Details + } + return nil +} + +pub fun getRaffleEntries(_ acct: Test.Account, _ id: UInt64): [AnyStruct]? { + if let res = scriptExecutor("get_raffle_entries.cdc", [acct.address, id]) { + return res as! [AnyStruct] + } + return nil +} + +pub fun getRaffleEntriesCount(_ acct: Test.Account, _ id: UInt64): Int? { + if let res = scriptExecutor("get_num_raffle_entries.cdc", [acct.address, id]) { + return res as! Int + } + return nil +} + +pub fun addEntryToRaffle(_ acct: Test.Account, _ id: UInt64, _ entry: AnyStruct) { + txExecutor("add_entry_to_raffle.cdc", [acct], [id, entry], nil) +} + +pub fun addEntriesToRaffle(_ acct: Test.Account, _ id: UInt64, _ entries: [AnyStruct]) { + txExecutor("add_entries_to_raffle.cdc", [acct], [id, entries], nil) +} + +pub fun createAddressRaffle(_ acct: Test.Account): UInt64 { + let name = "address raffle" + let description = "address raffle desc" + let thumbnail = "https://example.com/thumbnail" + let start: UInt64? = nil + let end: UInt64? = nil + + txExecutor("create_raffle.cdc", [acct], [Type
(), start, end, name, description, thumbnail], nil) + let createEvent = (Test.eventsOfType(Type()).removeLast() as! Raffles.RaffleCreated) + return createEvent.raffleID +} + +pub fun drawFromRaffle(_ signer: Test.Account, _ addr: Address, _ id: UInt64): String { + txExecutor("draw_from_raffle.cdc", [signer], [addr, id], nil) + + let drawingEvent = Test.eventsOfType(Type()).removeLast() as! Raffles.RaffleDrawn + return drawingEvent.value } \ No newline at end of file diff --git a/transactions/add_entries_to_raffle.cdc b/transactions/add_entries_to_raffle.cdc new file mode 100644 index 0000000..cb17a3f --- /dev/null +++ b/transactions/add_entries_to_raffle.cdc @@ -0,0 +1,11 @@ +import "Raffles" + +transaction(raffleID: UInt64, entries: [AnyStruct]) { + prepare(acct: AuthAccount) { + let manager = acct.borrow<&Raffles.Manager>(from: Raffles.ManagerStoragePath) + ?? panic("raffles manager not found") + let raffle = manager.borrowRaffle(id: raffleID) + ?? panic("raffle not found") + raffle.addEntries(entries) + } +} \ No newline at end of file diff --git a/transactions/add_entry_to_raffle.cdc b/transactions/add_entry_to_raffle.cdc new file mode 100644 index 0000000..85a5050 --- /dev/null +++ b/transactions/add_entry_to_raffle.cdc @@ -0,0 +1,11 @@ +import "Raffles" + +transaction(raffleID: UInt64, entry: AnyStruct) { + prepare(acct: AuthAccount) { + let manager = acct.borrow<&Raffles.Manager>(from: Raffles.ManagerStoragePath) + ?? panic("raffles manager not found") + let raffle = manager.borrowRaffle(id: raffleID) + ?? panic("raffle not found") + raffle.addEntry(entry) + } +} \ No newline at end of file diff --git a/transactions/create_raffle.cdc b/transactions/create_raffle.cdc new file mode 100644 index 0000000..958bff6 --- /dev/null +++ b/transactions/create_raffle.cdc @@ -0,0 +1,28 @@ +import "Raffles" +import "RaffleSources" +import "MetadataViews" + +transaction(type: Type, start: UInt64?, end: UInt64?, name: String, description: String, thumbnail: String) { + prepare(acct: AuthAccount) { + let source <- RaffleSources.createRaffleSource(type) + + if acct.borrow<&AnyResource>(from: Raffles.ManagerStoragePath) == nil { + acct.save(<-Raffles.createManager(), to: Raffles.ManagerStoragePath) + acct.link<&Raffles.Manager{Raffles.ManagerPublic}>(Raffles.ManagerPublicPath, target: Raffles.ManagerStoragePath) + } + + let manager = acct.borrow<&Raffles.Manager>(from: Raffles.ManagerStoragePath) + ?? panic("raffles manager not found") + + let display = MetadataViews.Display( + name: name, + description: description, + thumbnail: MetadataViews.HTTPFile(thumbnail) + ) + let details = Raffles.Details(start, end, display) + let id = manager.createRaffle(source: <-source, details: details) + + // make sure you can borrow the raffle back + manager.borrowRafflePublic(id: id) ?? panic("raffle not found") + } +} \ No newline at end of file diff --git a/transactions/create_raffle_source.cdc b/transactions/create_raffle_source.cdc new file mode 100644 index 0000000..e7e431e --- /dev/null +++ b/transactions/create_raffle_source.cdc @@ -0,0 +1,11 @@ +import "RaffleSources" + +transaction(type: Type, path: StoragePath) { + prepare(acct: AuthAccount) { + let source <- RaffleSources.createRaffleSource(type) + let t = source.getEntryType() + assert(t == type) + + acct.save(<-source, to: path) + } +} \ No newline at end of file diff --git a/transactions/draw_from_raffle.cdc b/transactions/draw_from_raffle.cdc new file mode 100644 index 0000000..3725c04 --- /dev/null +++ b/transactions/draw_from_raffle.cdc @@ -0,0 +1,12 @@ +import "Raffles" + +transaction(addr: Address, id: UInt64) { + prepare(acct: AuthAccount) { } + execute { + let manager = getAccount(addr).getCapability<&Raffles.Manager{Raffles.ManagerPublic}>(Raffles.ManagerPublicPath).borrow() + ?? panic("raffles manager not found") + let raffle = manager.borrowRafflePublic(id: id) + ?? panic("raffle not found") + raffle.draw() + } +} \ No newline at end of file