diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml new file mode 100644 index 0000000..3a63310 --- /dev/null +++ b/.github/workflows/integration-tests.yml @@ -0,0 +1,25 @@ +name: Integration Tests + +on: + pull_request: + branches: [main] + push: + branches: [main] + + +jobs: + tests: + name: Flow CLI Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: "true" + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: 1.18 + - name: Install Flow CLI + run: bash -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" -- v1.8.0 + - name: Run tests + run: sh ./run-tests.sh \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..256af5c --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +coverage.json +coverage.lcov +.idea +*.pkey +*.private +*.pem + +node_modules \ No newline at end of file diff --git a/contracts/Raffles.cdc b/contracts/Raffles.cdc new file mode 100644 index 0000000..d47cce3 --- /dev/null +++ b/contracts/Raffles.cdc @@ -0,0 +1,153 @@ +import "MetadataViews" + +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 struct Details { + pub let start: UInt64? + pub let end: UInt64? + pub let display: MetadataViews.Display + + init( + _ start: UInt64?, + _ end: UInt64, + _ display: MetadataViews.Display + + ) { + self.start = start + self.end = end + self.display = display + } + } + + pub resource interface RafflePublic { + pub fun getEntryAt(index: Int): 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 getEntryAt(index: Int): AnyStruct { + return self.source.getEntryAt(index: index) + } + + pub fun getViews(): [Type] { + return [ + Type() + ] + } + + pub fun resolveView(_ type: Type): AnyStruct? { + switch type { + case Type(): + return self.details.display + } + + return nil + } + + init( + source: @{RaffleSource}, + details: Details + ) { + self.source <- source + self.details = details + } + + destroy() { + destroy self.source + } + } + + 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}? + } + + pub resource Manager: ManagerPublic { + access(self) let raffles: @{UInt64: Raffle} + + pub fun borrowRafflePublic(id: UInt64): &{RafflePublic}? { + if self.raffles[id] == nil { + return nil + } + + return &self.raffles[id] as &Raffle? + } + + init() { + self.raffles <- {} + } + + destroy () { + destroy self.raffles + } + } + + access(contract) fun emitDrawing(address: Address, raffleID: UInt64, sourceType: Type, index: Int, value: AnyStruct) { + var v = "UNKNOWN" + switch value.getType() { + case Type
(): + v = (value as! Address).toString() + break + case Type(): + v = (value as! UInt64).toString() + break + } + + emit RaffleDrawn(address: address, raffleID: raffleID, sourceType: sourceType, index: index, value: v) + } + + 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)! + self.ManagerPublicPath = PublicPath(identifier: identifier)! + } +} \ No newline at end of file diff --git a/flow.json b/flow.json new file mode 100644 index 0000000..d993d07 --- /dev/null +++ b/flow.json @@ -0,0 +1,113 @@ +{ + "networks": { + "emulator": "127.0.0.1:3569", + "mainnet": "access.mainnet.nodes.onflow.org:9000", + "testing": "127.0.0.1:3569", + "testnet": "access.devnet.nodes.onflow.org:9000" + }, + "accounts": { + "emulator-account": { + "address": "0xf8d6e0586b0a20c7", + "key": "f2e846bd4c1fbf17839ae59e111c6b1c98579eda7a841412f102d6621ec671cb" + }, + "emulator-ft": { + "address": "ee82856bf20e2aa6", + "key": "f2e846bd4c1fbf17839ae59e111c6b1c98579eda7a841412f102d6621ec671cb" + }, + "emulator-flowtoken": { + "address": "0ae53cb6e3f42a79", + "key": "f2e846bd4c1fbf17839ae59e111c6b1c98579eda7a841412f102d6621ec671cb" + } + }, + "contracts": { + "Raffles": { + "source": "./contracts/Raffles.cdc", + "aliases": { + "testing": "0x0000000000000007" + } + }, + "FungibleToken": { + "source": "./node_modules/@flowtyio/flow-contracts/contracts/FungibleToken.cdc", + "aliases": { + "emulator": "0xee82856bf20e2aa6", + "testnet": "0x9a0766d93b6608b7", + "mainnet": "0xf233dcee88fe0abe" + } + }, + "MetadataViews": { + "source": "./node_modules/@flowtyio/flow-contracts/contracts/MetadataViews.cdc", + "aliases": { + "emulator": "0xf8d6e0586b0a20c7", + "testnet": "0x631e88ae7f1d7c20", + "mainnet": "0x1d7e57aa55817448" + } + }, + "FungibleTokenMetadataViews": { + "source": "./node_modules/@flowtyio/flow-contracts/contracts/FungibleTokenMetadataViews.cdc", + "aliases": { + "emulator": "0xee82856bf20e2aa6", + "testnet": "0x9a0766d93b6608b7", + "mainnet": "0xf233dcee88fe0abe" + } + }, + "ViewResolver": { + "source": "./node_modules/@flowtyio/flow-contracts/contracts/ViewResolver.cdc", + "aliases": { + "emulator": "0xf8d6e0586b0a20c7", + "testnet": "0x631e88ae7f1d7c20", + "mainnet": "0x1d7e57aa55817448" + } + }, + "NonFungibleToken": { + "source": "./node_modules/@flowtyio/flow-contracts/contracts/NonFungibleToken.cdc", + "aliases": { + "emulator": "0xf8d6e0586b0a20c7", + "testnet": "0x631e88ae7f1d7c20", + "mainnet": "0x1d7e57aa55817448" + } + }, + "FlowToken": { + "source": "./node_modules/@flowtyio/flow-contracts/contracts/FlowToken.cdc", + "aliases": { + "emulator": "0x0ae53cb6e3f42a79", + "testnet": "0x7e60df042a9c0868", + "mainnet": "0x1654653399040a61" + } + }, + "FindViews": { + "source": "./node_modules/@flowtyio/flow-contracts/contracts/find/FindViews.cdc", + "aliases": { + "emulator": "0xf8d6e0586b0a20c7", + "mainnet": "0x097bafa4e0b48eef", + "testnet": "0x35717efbbce11c74", + "testing": "0x0000000000000005" + } + }, + "FLOAT": { + "source": "./node_modules/@flowtyio/flow-contracts/contracts/emerald-city/FLOAT.cdc", + "aliases": { + "emulator": "0xf8d6e0586b0a20c7", + "mainnet": "0x2d4c3caffbeab845", + "testnet": "0x4d47bf3ce5e4393f", + "testing": "0x0000000000000005" + } + } + }, + "deployments": { + "emulator": { + "emulator-account": [ + "MetadataViews", + "ViewResolver", + "NonFungibleToken", + "Raffles" + ], + "emulator-ft": [ + "FungibleToken", + "FungibleTokenMetadataViews" + ], + "emulator-flowtoken": [ + "FlowToken" + ] + } + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..7bdf8c8 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,46 @@ +{ + "name": "flow-raffle", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "dependencies": { + "@flowtyio/flow-contracts": "^0.0.18" + } + }, + "node_modules/@flowtyio/flow-contracts": { + "version": "0.0.18", + "resolved": "https://registry.npmjs.org/@flowtyio/flow-contracts/-/flow-contracts-0.0.18.tgz", + "integrity": "sha512-uQVbUOZegx8sdk/T+ypYBjdVmcw7AGHANzrg+PdQCtYXBGJ3iEh9+vxwf2kNk2j6hw8djmWdZy36iUMZC3pwmg==", + "dependencies": { + "commander": "^11.0.0" + }, + "bin": { + "flow-contracts": "index.js" + } + }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "engines": { + "node": ">=16" + } + } + }, + "dependencies": { + "@flowtyio/flow-contracts": { + "version": "0.0.18", + "resolved": "https://registry.npmjs.org/@flowtyio/flow-contracts/-/flow-contracts-0.0.18.tgz", + "integrity": "sha512-uQVbUOZegx8sdk/T+ypYBjdVmcw7AGHANzrg+PdQCtYXBGJ3iEh9+vxwf2kNk2j6hw8djmWdZy36iUMZC3pwmg==", + "requires": { + "commander": "^11.0.0" + } + }, + "commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..790bde2 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "@flowtyio/flow-contracts": "^0.0.18" + } +} diff --git a/run-tests.sh b/run-tests.sh new file mode 100755 index 0000000..acc7741 --- /dev/null +++ b/run-tests.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +set -e + +flow test --cover --covercode="contracts" --coverprofile="coverage.lcov" test/*_tests.cdc \ No newline at end of file diff --git a/scripts/borrow_manager.cdc b/scripts/borrow_manager.cdc new file mode 100644 index 0000000..9ed79c8 --- /dev/null +++ b/scripts/borrow_manager.cdc @@ -0,0 +1,6 @@ +import "Raffles" + +pub fun main(addr: Address) { + getAccount(addr).getCapability<&Raffles.Manager{Raffles.ManagerPublic}>(Raffles.ManagerPublicPath).borrow() + ?? panic("unable to borrow manager") +} \ No newline at end of file diff --git a/test/Raffles_tests.cdc b/test/Raffles_tests.cdc new file mode 100644 index 0000000..baf3a55 --- /dev/null +++ b/test/Raffles_tests.cdc @@ -0,0 +1,16 @@ +import Test +import "test_helpers.cdc" + +import "Raffles" + + +pub fun setup() { + let err = Test.deployContract(name: "Raffles", path: "../contracts/Raffles.cdc", arguments: []) + Test.expect(err, Test.beNil()) +} + +pub fun setupManager() { + let acct = Test.createAccount() + txExecutor("setup_manager", [acct], [], nil) + scriptExecutor("borrow_manager.cdc", [acct.address]) +} \ No newline at end of file diff --git a/test/test_helpers.cdc b/test/test_helpers.cdc new file mode 100644 index 0000000..2936a80 --- /dev/null +++ b/test/test_helpers.cdc @@ -0,0 +1,62 @@ +import Test + +/* + Most of this was taken from: + https://github.com/onflow/hybrid-custody/blob/main/test/test_helpers.cdc +*/ + +pub fun loadCode(_ fileName: String, _ baseDirectory: String): String { + return Test.readFile("../".concat(baseDirectory).concat("/").concat(fileName)) +} + +pub fun scriptExecutor(_ scriptName: String, _ arguments: [AnyStruct]): AnyStruct? { + let scriptCode = loadCode(scriptName, "scripts") + let scriptResult = Test.executeScript(scriptCode, arguments) + + if let failureError = scriptResult.error { + panic( + "Failed to execute the script because -: ".concat(failureError.message) + ) + } + + return scriptResult.returnValue +} + +pub fun txExecutor( + _ txName: String, + _ signers: [Test.Account], + _ arguments: [AnyStruct], + _ expectedError: String? +): Bool { + let txCode = loadCode(txName, "transactions") + + let authorizers: [Address] = [] + for signer in signers { + authorizers.append(signer.address) + } + + let tx = Test.Transaction( + code: txCode, + authorizers: authorizers, + signers: signers, + arguments: arguments, + ) + + let txResult = Test.executeTransaction(tx) + if let err = txResult.error { + if let expectedErrorMessage = expectedError { + Test.assertError( + txResult, + errorMessage: expectedErrorMessage + ) + return true + } + } else { + if let expectedErrorMessage = expectedError { + panic("Expecting error - ".concat(expectedErrorMessage).concat(". While no error triggered")) + } + } + + Test.expect(txResult, Test.beSucceeded()) + return true +} \ No newline at end of file diff --git a/transactions/setup_manager.cdc b/transactions/setup_manager.cdc new file mode 100644 index 0000000..93371a8 --- /dev/null +++ b/transactions/setup_manager.cdc @@ -0,0 +1,9 @@ +import "Raffles" + +transaction { + prepare(acct: AuthAccount) { + let manager <- Raffles.createManager() + acct.save(<-manager, to: Raffles.ManagerStoragePath) + acct.link<&Raffles.Manager{Raffles.ManagerPublic}>(Raffles.ManagerPublicPath, target: Raffles.ManagerStoragePath) + } +} \ No newline at end of file