Skip to content

Commit

Permalink
Add borrowNFTSafe impl and test (#206)
Browse files Browse the repository at this point in the history
  • Loading branch information
jrkhan authored Jan 26, 2023
1 parent ac8329d commit b824c6f
Show file tree
Hide file tree
Showing 7 changed files with 204 additions and 5 deletions.
13 changes: 13 additions & 0 deletions contracts/TopShot.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -1254,6 +1254,19 @@ pub contract TopShot: NonFungibleToken {
return (&self.ownedNFTs[id] as &NonFungibleToken.NFT?)!
}

// Safe way to borrow a reference to an NFT that does not panic
// Also now part of the NonFungibleToken.PublicCollection interface
//
// Parameters: id: The ID of the NFT to get the reference for
//
// Returns: An optional reference to the desired NFT, will be nil if the passed ID does not exist
pub fun borrowNFTSafe(id: UInt64): &NonFungibleToken.NFT? {
if let nftRef = &self.ownedNFTs[id] as &NonFungibleToken.NFT? {
return nftRef
}
return nil
}

// borrowMoment returns a borrowed reference to a Moment
// so that the caller can read data and call methods from it.
// They can use this to read its setID, playID, serialNumber,
Expand Down
6 changes: 3 additions & 3 deletions lib/go/contracts/internal/assets/assets.go

Large diffs are not rendered by default.

23 changes: 23 additions & 0 deletions lib/go/templates/internal/assets/assets.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions lib/go/templates/topshot_script_templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const (
momentSerialNumFilename = "collections/get_moment_serialNum.cdc"
momentSetNameFilename = "collections/get_moment_setName.cdc"
getSetPlaysAreOwnedFilename = "collections/get_setplays_are_owned.cdc"
borrowNFTSafeFilename = "collections/borrow_nft_safe.cdc"

// metadata scripts
getNFTMetadataFilename = "get_nft_metadata.cdc"
Expand Down Expand Up @@ -265,3 +266,9 @@ func GenerateGetSubeditionByIDScript(env Environment) []byte {

return []byte(replaceAddresses(code, env))
}

func GenerateBorrowNFTSafeScript(env Environment) []byte {
code := assets.MustAssetString(scriptsPath + borrowNFTSafeFilename)

return []byte(replaceAddresses(code, env))
}
109 changes: 109 additions & 0 deletions lib/go/test/borrowNFT_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package test

import (
"testing"

"github.com/onflow/cadence"
jsoncdc "github.com/onflow/cadence/encoding/json"

"github.com/dapperlabs/nba-smart-contracts/lib/go/templates"

"github.com/onflow/flow-go-sdk/crypto"

"github.com/onflow/flow-go-sdk"
"github.com/stretchr/testify/assert"
)

func TestBorrowNFTSafe(t *testing.T) {
tb := NewTopShotTestBlockchain(t)
tb.genericBootstrapping(t) // TODO: Should be able to find a codeGen/declarative way to set things up
b := tb.Blockchain
env := tb.env

t.Run("Should return non nil if the moment id in the collection", func(t *testing.T) {
for _, momentID := range []uint64{1, 2} {
r, err := b.ExecuteScript(templates.GenerateBorrowNFTSafeScript(env), [][]byte{jsoncdc.MustEncode(cadence.Address(tb.userAddress)), jsoncdc.MustEncode(cadence.UInt64(momentID))})
assert.NoError(t, err)
assert.NoError(t, r.Error)
expectedValue := cadence.NewBool(true)
assert.Equal(t, expectedValue, r.Value)
}
})

t.Run("Should return nil/empty optional if the moment does not exist in the collection", func(t *testing.T) {
r, err := b.ExecuteScript(templates.GenerateBorrowNFTSafeScript(env), [][]byte{jsoncdc.MustEncode(cadence.Address(tb.userAddress)), jsoncdc.MustEncode(cadence.UInt64(3))})
assert.NoError(t, err)
assert.NoError(t, r.Error)
expectedValue := cadence.NewBool(false)
assert.Equal(t, expectedValue, r.Value)
})
}

// genericBootstrapping should get us the blockchain in a state where we can run interesting tests against it
// will need to likely expose more of the generated ids for this to be generally useful
func (tb *topshotTestBlockchain) genericBootstrapping(t *testing.T) {
b := tb.Blockchain
serviceKeySigner := tb.serviceKeySigner
topshotAddr := tb.topshotAdminAddr
accountKeys := tb.accountKeys
topshotSigner := tb.topshotAdminSigner
env := tb.env

// Create a new user account
joshAccountKey, joshSigner := accountKeys.NewWithSigner()
joshAddress, _ := b.CreateAccount([]*flow.AccountKey{joshAccountKey}, nil)
tb.userAddress = joshAddress
// Create moment collection
tx := createTxWithTemplateAndAuthorizer(b, templates.GenerateSetupAccountScript(env), joshAddress)
signAndSubmit(
t, b, tx,
[]flow.Address{b.ServiceKey().Address, joshAddress}, []crypto.Signer{serviceKeySigner, joshSigner},
false,
)

firstName := CadenceString("FullName")
lebron := CadenceString("Lebron")
hayward := CadenceString("Hayward")
antetokounmpo := CadenceString("Antetokounmpo")

// Create plays
lebronPlayID := uint32(1)
haywardPlayID := uint32(2)
antetokounmpoPlayID := uint32(3)

for _, metadata := range [][]cadence.KeyValuePair{
{{Key: firstName, Value: lebron}},
{{Key: firstName, Value: hayward}},
{{Key: firstName, Value: antetokounmpo}},
} {
tb.CreatePlay(t, metadata)
}

// Create Set
genesisSetID := uint32(1)
tb.CreateSet(t, "Genesis")

// Add plays to Set
tx = createTxWithTemplateAndAuthorizer(b, templates.GenerateAddPlaysToSetScript(env), topshotAddr)

_ = tx.AddArgument(cadence.NewUInt32(genesisSetID))

plays := []cadence.Value{cadence.NewUInt32(lebronPlayID), cadence.NewUInt32(haywardPlayID), cadence.NewUInt32(antetokounmpoPlayID)}
_ = tx.AddArgument(cadence.NewArray(plays))

signAndSubmit(
t, b, tx,
[]flow.Address{b.ServiceKey().Address, topshotAddr}, []crypto.Signer{serviceKeySigner, topshotSigner},
false,
)

// Mint two moments to joshAddress
tb.MintMoment(t, genesisSetID, lebronPlayID, joshAddress)
tb.MintMoment(t, genesisSetID, haywardPlayID, joshAddress)

//check that moments with ids 1 and 2 exist in josh's collection
result := executeScriptAndCheck(t, b, templates.GenerateIsIDInCollectionScript(env), [][]byte{jsoncdc.MustEncode(cadence.Address(joshAddress)), jsoncdc.MustEncode(cadence.UInt64(1))})
assert.Equal(t, cadence.NewBool(true), result)
result = executeScriptAndCheck(t, b, templates.GenerateIsIDInCollectionScript(env), [][]byte{jsoncdc.MustEncode(cadence.Address(joshAddress)), jsoncdc.MustEncode(cadence.UInt64(2))})
assert.Equal(t, cadence.NewBool(true), result)
}
19 changes: 17 additions & 2 deletions lib/go/test/topshot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ type topshotTestBlockchain struct {
serviceKeySigner crypto.Signer
topshotAdminSigner crypto.Signer
accountKeys *test.AccountKeys

userAddress flow.Address
}

func NewTopShotTestBlockchain(t *testing.T) topshotTestBlockchain {
Expand Down Expand Up @@ -141,7 +143,13 @@ func NewTopShotTestBlockchain(t *testing.T) topshotTestBlockchain {

env.ShardedAddress = shardedAddr.String()

return topshotTestBlockchain{b, env, topshotAddr, serviceKeySigner, topshotSigner, accountKeys}
return topshotTestBlockchain{
Blockchain: b,
env: env,
topshotAdminAddr: topshotAddr,
serviceKeySigner: serviceKeySigner,
topshotAdminSigner: topshotSigner,
accountKeys: accountKeys}
}

// This test is for testing the deployment the topshot smart contracts
Expand Down Expand Up @@ -762,7 +770,14 @@ func TestTransferAdmin(t *testing.T) {
)
})

tb := topshotTestBlockchain{b, env, adminAddr, serviceKeySigner, adminSigner, accountKeys}
tb := topshotTestBlockchain{
Blockchain: b,
env: env,
topshotAdminAddr: adminAddr,
serviceKeySigner: serviceKeySigner,
topshotAdminSigner: adminSigner,
accountKeys: accountKeys,
}
// can create a new play with the new admin
t.Run("Should be able to create a new Play with the new Admin account", func(t *testing.T) {
tb.CreatePlay(t, []cadence.KeyValuePair{{Key: firstName, Value: lebron}})
Expand Down
32 changes: 32 additions & 0 deletions transactions/scripts/collections/borrow_nft_safe.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import TopShot from 0xTOPSHOTADDRESS
import NonFungibleToken from 0xNFTADDRESS

// This is a script to get a boolean value safely to see if a moment exists in a collection
// We expect this will not panic if the NFT is not in the collection
// Change the `account` to whatever account you want
// and as long as they have a published Collection receiver, you can
// get reference to the NFTs they own.
// Parameters:
//
// account: The Flow Address of the account whose moment data needs to be read
// nftID: The ID of the NFT to return
// Returns: Boolean value indicating if the NFT is in the collection
pub fun main(account: Address, nftID: UInt64 ): Bool {

let acct = getAccount(account)

let collectionRef = acct.getCapability(/public/MomentCollection)
.borrow<&{NonFungibleToken.CollectionPublic}>()!

let optionalNFT = collectionRef.borrowNFTSafe(id: nftID)

// optional binding
if let nft = optionalNFT {
return true
} else {
return false
}
}

0 comments on commit b824c6f

Please sign in to comment.