From 2f70167198a5f440f97e2d6efaff1fedd6790d1e Mon Sep 17 00:00:00 2001 From: Innocent Abdullahi Date: Mon, 7 Oct 2024 12:03:37 -0700 Subject: [PATCH] allow escrow admin transfer moments to any account (#81) * allow escrow admin transfer moments to any account * Update Escrow.cdc * escrow test * trigger test * Create Makefile * Update Escrow.cdc * Update templates.go --- .github/workflows/escrow-test.yml | 15 +++ escrow/Makefile | 7 + escrow/contracts/Escrow.cdc | 44 +++++-- escrow/lib/go/test/Makefile | 2 +- escrow/lib/go/test/escrow_test.go | 122 +++++++++++++++++- escrow/lib/go/test/scripts.go | 2 +- escrow/lib/go/test/templates.go | 28 ++-- escrow/lib/go/test/test.go | 2 +- escrow/lib/go/test/transactions.go | 28 ++++ escrow/lib/go/test/types.go | 2 +- .../admin/leaderboards/admin_transfer.cdc | 22 ++++ 11 files changed, 245 insertions(+), 29 deletions(-) create mode 100644 .github/workflows/escrow-test.yml create mode 100644 escrow/Makefile create mode 100644 escrow/transactions/admin/leaderboards/admin_transfer.cdc diff --git a/.github/workflows/escrow-test.yml b/.github/workflows/escrow-test.yml new file mode 100644 index 0000000..d882d4a --- /dev/null +++ b/.github/workflows/escrow-test.yml @@ -0,0 +1,15 @@ +name: Test + +on: + push: + paths: + - "escrow/**" +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-go@v1 + with: + go-version: '1.21' + - run: cd escrow && make test \ No newline at end of file diff --git a/escrow/Makefile b/escrow/Makefile new file mode 100644 index 0000000..9805c77 --- /dev/null +++ b/escrow/Makefile @@ -0,0 +1,7 @@ +.PHONY: test +test: + $(MAKE) test -C ./lib/go + +.PHONY: ci +ci: + $(MAKE) ci -C ./lib/go \ No newline at end of file diff --git a/escrow/contracts/Escrow.cdc b/escrow/contracts/Escrow.cdc index 1c3d89e..1005241 100644 --- a/escrow/contracts/Escrow.cdc +++ b/escrow/contracts/Escrow.cdc @@ -74,19 +74,11 @@ access(all) contract Escrow { emit EntryDeposited(leaderboardName: self.name, nftID: nftID, owner: ownerAddress) } - // Withdraws an NFT entry from the leaderboard. + // Transfers an NFT entry from the leaderboard to an account. access(contract) fun transferNftToCollection(nftID: UInt64, depositCap: Capability<&{NonFungibleToken.Collection}>) { - pre { - depositCap.check() : "Deposit capability is not valid" - } - if(self.entriesData[nftID] == nil) { - return - } - if(depositCap.address != self.entriesData[nftID]!.ownerAddress){ - panic("Only the owner of the entry can withdraw it") - } - - + pre { + depositCap.check() : "Deposit capability is not valid" + } // Remove the NFT entry's data from the leaderboard. self.entriesData.remove(key: nftID)! @@ -195,14 +187,38 @@ access(all) contract Escrow { leaderboard.addEntryToLeaderboard(nft: <-nft, ownerAddress: ownerAddress) } - // Calls transferNftToCollection. + // transferNftToCollection transfers the moment back to the owner's collection once the leaderboard is completed. + // This is used for entries of users who didn't win the leaderboard access(Operate) fun transferNftToCollection(leaderboardName: String, nftID: UInt64, depositCap: Capability<&{NonFungibleToken.Collection}>) { let leaderboard= &self.leaderboards[leaderboardName] as &Leaderboard? ?? panic("Leaderboard does not exist with this name") + + if(leaderboard.entriesData[nftID] == nil) { + return + } + + if(depositCap.address != leaderboard.entriesData[nftID]!.ownerAddress){ + panic("Only the owner of the entry can withdraw it") + } + + leaderboard.transferNftToCollection(nftID: nftID, depositCap: depositCap) + } + + // adminTransferNftToCollection transfers the moment back to an admin collection once the leaderboard is completed. + // This is used for entries of users who won the leaderboard + access(Operate) fun adminTransferNftToCollection(leaderboardName: String, nftID: UInt64, depositCap: Capability<&{NonFungibleToken.Collection}>) { + let leaderboard= &self.leaderboards[leaderboardName] as &Leaderboard? + ?? panic("Leaderboard does not exist with this name") + + if(leaderboard.entriesData[nftID] == nil) { + return + } + leaderboard.transferNftToCollection(nftID: nftID, depositCap: depositCap) } - // Calls burn. + // burn destroys the NFT from the leaderboard. + // this is used for entries of users who won the leaderboard access(Operate) fun burn(leaderboardName: String, nftID: UInt64) { let leaderboard = &self.leaderboards[leaderboardName] as &Leaderboard? ?? panic("Leaderboard does not exist with this name") diff --git a/escrow/lib/go/test/Makefile b/escrow/lib/go/test/Makefile index c620d5f..105d032 100644 --- a/escrow/lib/go/test/Makefile +++ b/escrow/lib/go/test/Makefile @@ -1,6 +1,6 @@ .PHONY: test test: - go test ./... + CGO_ENABLED=0 go test -tags=no_cgo ./... .PHONY: ci ci: test diff --git a/escrow/lib/go/test/escrow_test.go b/escrow/lib/go/test/escrow_test.go index de0e0a6..622343d 100644 --- a/escrow/lib/go/test/escrow_test.go +++ b/escrow/lib/go/test/escrow_test.go @@ -527,7 +527,7 @@ func TestEscrow(t *testing.T) { t.Run("Should get the leaderboard by name to confirm it exists", func(t *testing.T) { // Get leaderboard data from the contract. leaderboard, _ := getLeaderboardData(t, b, contracts, "leaderboardBurn-1") - assert.Equal(t, "\"leaderboardBurn-1\"", leaderboard.Name) + assert.Equal(t, "leaderboardBurn-1", leaderboard.Name) assert.Equal(t, "Type()", leaderboard.NftType) assert.Equal(t, uint64(0), leaderboard.EntriesLength) }) @@ -624,6 +624,108 @@ func TestEscrow(t *testing.T) { }) } +func TestEscrowAdminTransfer(t *testing.T) { + b := newEmulator() + contracts := EscrowContracts(t, b) + userAddress, userSigner := createAccount(t, b) + setupAllDay(t, b, userAddress, userSigner, contracts) + + createTestEditions(t, b, contracts) + + t.Run("Should be able to mint a new MomentNFT from an edition that has a maxMintSize", func(t *testing.T) { + testMintMomentNFT( + t, + b, + contracts, + uint64(1), + nil, + userAddress, + uint64(1), + uint64(1), + false, + ) + }) + + t.Run("Should confirm that 1 MomentNFT exists within users collection", func(t *testing.T) { + // Get the MomentNFT data from the users collection. + count := getMomentNFTLengthInAccount(t, b, contracts, userAddress) + assert.Equal(t, big.NewInt(1), count) + }) + + t.Run("Should be able to create a leaderboard", func(t *testing.T) { + testCreateLeaderboard( + t, + b, + contracts, + "leaderboardBurn-1", + ) + }) + + t.Run("Should get the leaderboard by name to confirm it exists", func(t *testing.T) { + // Get leaderboard data from the contract. + leaderboard, _ := getLeaderboardData(t, b, contracts, "leaderboardBurn-1") + assert.Equal(t, "leaderboardBurn-1", leaderboard.Name) + assert.Equal(t, "Type()", leaderboard.NftType) + assert.Equal(t, uint64(0), leaderboard.EntriesLength) + }) + + t.Run("Should be able to escrow moment to leaderboard", func(t *testing.T) { + testEscrowMomentNFT( + t, + b, + contracts, + userSigner, + userAddress, + uint64(1), + ) + }) + + t.Run("Should get the leaderboard by name to confirm entries", func(t *testing.T) { + // Get leaderboard data from the contract. + leaderboard, _ := getLeaderboardData(t, b, contracts, "leaderboardBurn-1") + assert.Equal(t, uint64(1), leaderboard.EntriesLength) + }) + + t.Run("Should confirm that 0 MomentNFTs exists within users collection due to escrow", func(t *testing.T) { + // Get the MomentNFT data from the users collection. + count := getMomentNFTLengthInAccount(t, b, contracts, userAddress) + assert.Equal(t, big.NewInt(0), count) + }) + + var ( + newUserAddress flow.Address + newUserSigner crypto.Signer + ) + t.Run("Should create another account and setup allDat collection", func(t *testing.T) { + newUserAddress, newUserSigner = createAccount(t, b) + setupAllDay(t, b, newUserAddress, newUserSigner, contracts) + }) + + t.Run("Should admin transfer moment to new account from Leaderboard by name", func(t *testing.T) { + testAdminTransferMomentNFT( + t, + b, + contracts, + "leaderboardBurn-1", + newUserAddress, + uint64(1), + ) + }) + + t.Run("Should check that the MomentNFT is in the new user's collection", func(t *testing.T) { + // Get the MomentNFT data from the users collection. + count := getMomentNFTLengthInAccount(t, b, contracts, newUserAddress) + assert.Equal(t, big.NewInt(1), count) + }) + + t.Run("Should get the leaderboard by name to confirm entries", func(t *testing.T) { + // Get leaderboard data from the contract. + leaderboard, _ := getLeaderboardData(t, b, contracts, "leaderboardBurn-1") + assert.Equal(t, uint64(0), leaderboard.EntriesLength) + }) + +} + func testCreateLeaderboard( t *testing.T, b *emulator.Blockchain, @@ -672,6 +774,24 @@ func testBurnMomentNFT( ) } +func testAdminTransferMomentNFT( + t *testing.T, + b *emulator.Blockchain, + contracts Contracts, + leaderboardName string, + userAddress flow.Address, + momentNftFlowID uint64, +) { + adminTransferMomentNFT( + t, + b, + contracts, + leaderboardName, + userAddress, + momentNftFlowID, + ) +} + func testEscrowMomentNFT( t *testing.T, b *emulator.Blockchain, diff --git a/escrow/lib/go/test/scripts.go b/escrow/lib/go/test/scripts.go index 3c18921..6191066 100644 --- a/escrow/lib/go/test/scripts.go +++ b/escrow/lib/go/test/scripts.go @@ -16,7 +16,7 @@ func accountIsSetup( contracts Contracts, address flow.Address, ) bool { - script := loadEscrowAccountIsSetupScript(contracts) + script := loadAllDayAccountIsSetupScript(contracts) result := executeScriptAndCheck(t, b, script, [][]byte{jsoncdc.MustEncode(cadence.BytesToAddress(address.Bytes()))}) return GetFieldValue(result).(bool) diff --git a/escrow/lib/go/test/templates.go b/escrow/lib/go/test/templates.go index e460b52..69763fc 100644 --- a/escrow/lib/go/test/templates.go +++ b/escrow/lib/go/test/templates.go @@ -26,8 +26,8 @@ const ( EscrowScriptsRootPath = "../../../scripts" // Accounts - EscrowSetupAccountPath = EscrowTransactionsRootPath + "/user/setup_AllDay_account.cdc" - EscrowAccountIsSetupPath = EscrowScriptsRootPath + "/user/account_is_setup.cdc" + AllDaySetupAccountPath = EscrowTransactionsRootPath + "/user/setup_allday_account.cdc" + AllDayAccountIsSetupPath = EscrowScriptsRootPath + "/user/account_is_setup.cdc" // Series EscrowCreateSeriesPath = EscrowTransactionsRootPath + "/admin/series/create_series.cdc" @@ -55,10 +55,11 @@ const ( EscrowReadCollectionLengthPath = EscrowScriptsRootPath + "/nfts/read_collection_nft_length.cdc" // Escrow - EscrowMomentNFTPath = EscrowTransactionsRootPath + "/user/add_entry.cdc" - EscrowWithdrawMomentNFTPath = EscrowTransactionsRootPath + "/admin/leaderboards/withdraw_entry.cdc" - EscrowBurnNFTPath = EscrowTransactionsRootPath + "/admin/leaderboards/burn_nft.cdc" - EscrowReadLeaderboardInfoPath = EscrowScriptsRootPath + "/leaderboards/read_leaderboard_info.cdc" + EscrowMomentNFTPath = EscrowTransactionsRootPath + "/user/add_entry.cdc" + EscrowWithdrawMomentNFTPath = EscrowTransactionsRootPath + "/admin/leaderboards/withdraw_entry.cdc" + EscrowAdminTransferMomentNFTPath = EscrowTransactionsRootPath + "/admin/leaderboards/admin_transfer.cdc" + EscrowBurnNFTPath = EscrowTransactionsRootPath + "/admin/leaderboards/burn_nft.cdc" + EscrowReadLeaderboardInfoPath = EscrowScriptsRootPath + "/leaderboards/read_leaderboard_info.cdc" ) // ------------------------------------------------------------ @@ -119,16 +120,16 @@ func LoadEscrow(nftAddress flow.Address) []byte { return code } -func loadEscrowSetupAccountTransaction(contracts Contracts) []byte { +func loadAllDaySetupAccountTransaction(contracts Contracts) []byte { return replaceAddresses( - readFile(EscrowSetupAccountPath), + readFile(AllDaySetupAccountPath), contracts, ) } -func loadEscrowAccountIsSetupScript(contracts Contracts) []byte { +func loadAllDayAccountIsSetupScript(contracts Contracts) []byte { return replaceAddresses( - readFile(EscrowAccountIsSetupPath), + readFile(AllDayAccountIsSetupPath), contracts, ) } @@ -256,6 +257,13 @@ func loadEscrowWithdrawMomentNFT(contracts Contracts) []byte { ) } +func loadEscrowAdminTransferMomentNFT(contracts Contracts) []byte { + return replaceAddresses( + readFile(EscrowAdminTransferMomentNFTPath), + contracts, + ) +} + func loadEscrowBurnNFTTransaction(contracts Contracts) []byte { return replaceAddresses( readFile(EscrowBurnNFTPath), diff --git a/escrow/lib/go/test/test.go b/escrow/lib/go/test/test.go index 7541141..76da9f0 100644 --- a/escrow/lib/go/test/test.go +++ b/escrow/lib/go/test/test.go @@ -270,7 +270,7 @@ func setupAllDay( contracts Contracts, ) { tx := flow.NewTransaction(). - SetScript(loadEscrowSetupAccountTransaction(contracts)). + SetScript(loadAllDaySetupAccountTransaction(contracts)). SetComputeLimit(100). SetProposalKey(b.ServiceKey().Address, b.ServiceKey().Index, b.ServiceKey().SequenceNumber). SetPayer(b.ServiceKey().Address). diff --git a/escrow/lib/go/test/transactions.go b/escrow/lib/go/test/transactions.go index 628a682..6459b2a 100644 --- a/escrow/lib/go/test/transactions.go +++ b/escrow/lib/go/test/transactions.go @@ -289,6 +289,34 @@ func withdrawMomentNFT( ) } +func adminTransferMomentNFT( + t *testing.T, + b *emulator.Blockchain, + contracts Contracts, + leaderboardName string, + userAddress flow.Address, + momentNftFlowID uint64, +) { + tx := flow.NewTransaction(). + SetScript(loadEscrowAdminTransferMomentNFT(contracts)). + SetComputeLimit(100). + SetProposalKey(b.ServiceKey().Address, b.ServiceKey().Index, b.ServiceKey().SequenceNumber). + SetPayer(b.ServiceKey().Address). + AddAuthorizer(contracts.AllDayAddress) + tx.AddArgument(cadence.String(leaderboardName)) + tx.AddArgument(cadence.NewUInt64(momentNftFlowID)) + tx.AddArgument(cadence.BytesToAddress(userAddress.Bytes())) + + signer, err := b.ServiceKey().Signer() + require.NoError(t, err) + signAndSubmit( + t, b, tx, + []flow.Address{b.ServiceKey().Address, contracts.AllDayAddress}, + []crypto.Signer{signer, contracts.AllDaySigner}, + false, + ) +} + func burnMomentNFT( t *testing.T, b *emulator.Blockchain, diff --git a/escrow/lib/go/test/types.go b/escrow/lib/go/test/types.go index 8d16a27..0906c19 100644 --- a/escrow/lib/go/test/types.go +++ b/escrow/lib/go/test/types.go @@ -158,7 +158,7 @@ func parseLeaderboardInfo(value cadence.Value) (LeaderboardInfo, error) { for k, v := range fields { switch k { case "name": - s.Name = v.(cadence.String).String() + s.Name = string(v.(cadence.String)) case "nftType": s.NftType = v.String() case "entriesLength": diff --git a/escrow/transactions/admin/leaderboards/admin_transfer.cdc b/escrow/transactions/admin/leaderboards/admin_transfer.cdc new file mode 100644 index 0000000..e882f2d --- /dev/null +++ b/escrow/transactions/admin/leaderboards/admin_transfer.cdc @@ -0,0 +1,22 @@ +import Escrow from "Escrow" +import AllDay from "AllDay" +import NonFungibleToken from "NonFungibleToken" + +// This transaction takes the leaderboardName and nftID and returns it to the correct owner. +transaction(leaderboardName: String, nftID: UInt64, ownerAddress: Address) { + prepare(signer: auth(BorrowValue) &Account) { + // Get a reference to the Collection resource in storage. + let collectionRef = signer.storage.borrow(from: Escrow.CollectionStoragePath) + ?? panic("Could not borrow reference to the Collection resource") + + let depositCap = getAccount(ownerAddress) + .capabilities.get<&{NonFungibleToken.Collection}>(AllDay.CollectionPublicPath) + + // Call transferNftToCollection function. + collectionRef.adminTransferNftToCollection(leaderboardName: leaderboardName, nftID: nftID, depositCap: depositCap) + } + + execute { + log("Withdrawn NFT from leaderboard") + } +}