diff --git a/cmd/util/ledger/migrations/account_storage_migration.go b/cmd/util/ledger/migrations/account_storage_migration.go index d96bfd5f83b..444819ffac6 100644 --- a/cmd/util/ledger/migrations/account_storage_migration.go +++ b/cmd/util/ledger/migrations/account_storage_migration.go @@ -52,25 +52,15 @@ func NewAccountStorageMigration( log.Err(err).Msg("storage health check failed") } - // Finalize the transaction - result, err := migrationRuntime.TransactionState.FinalizeMainTransaction() - if err != nil { - return fmt.Errorf("failed to finalize main transaction: %w", err) - } + // Commit/finalize the transaction - // Merge the changes into the registers expectedAddresses := map[flow.Address]struct{}{ flow.Address(address): {}, } - err = registers.ApplyChanges( - registersByAccount, - result.WriteSet, - expectedAddresses, - log, - ) + err = migrationRuntime.Commit(expectedAddresses, log) if err != nil { - return fmt.Errorf("failed to apply register changes: %w", err) + return fmt.Errorf("failed to commit: %w", err) } return nil diff --git a/cmd/util/ledger/migrations/cadence.go b/cmd/util/ledger/migrations/cadence.go index d78538b8d9a..e4f39665c7a 100644 --- a/cmd/util/ledger/migrations/cadence.go +++ b/cmd/util/ledger/migrations/cadence.go @@ -392,25 +392,15 @@ func (m *IssueStorageCapConMigration) MigrateAccount( return fmt.Errorf("failed to commit changes: %w", err) } - // finalize the transaction - result, err := migrationRuntime.TransactionState.FinalizeMainTransaction() - if err != nil { - return fmt.Errorf("failed to finalize main transaction: %w", err) - } + // Commit/finalize the transaction - // Merge the changes into the registers expectedAddresses := map[flow.Address]struct{}{ flow.Address(address): {}, } - err = registers.ApplyChanges( - accountRegisters, - result.WriteSet, - expectedAddresses, - m.log, - ) + err = migrationRuntime.Commit(expectedAddresses, m.log) if err != nil { - return fmt.Errorf("failed to apply changes: %w", err) + return fmt.Errorf("failed to commit: %w", err) } return nil diff --git a/cmd/util/ledger/migrations/cadence_values_migration.go b/cmd/util/ledger/migrations/cadence_values_migration.go index 2dad5f49eb6..4bd2c756cde 100644 --- a/cmd/util/ledger/migrations/cadence_values_migration.go +++ b/cmd/util/ledger/migrations/cadence_values_migration.go @@ -180,25 +180,15 @@ func (m *CadenceBaseMigration) MigrateAccount( } } - // finalize the transaction - result, err := migrationRuntime.TransactionState.FinalizeMainTransaction() - if err != nil { - return fmt.Errorf("failed to finalize main transaction: %w", err) - } + // Commit/finalize the transaction - // Merge the changes into the registers expectedAddresses := map[flow.Address]struct{}{ flow.Address(address): {}, } - err = registers.ApplyChanges( - accountRegisters, - result.WriteSet, - expectedAddresses, - m.log, - ) + err = migrationRuntime.Commit(expectedAddresses, m.log) if err != nil { - return fmt.Errorf("failed to apply changes: %w", err) + return fmt.Errorf("failed to commit: %w", err) } if m.diffReporter != nil { diff --git a/cmd/util/ledger/migrations/contract_checking_migration_test.go b/cmd/util/ledger/migrations/contract_checking_migration_test.go index 5f7cb84553a..1a4ffd957f2 100644 --- a/cmd/util/ledger/migrations/contract_checking_migration_test.go +++ b/cmd/util/ledger/migrations/contract_checking_migration_test.go @@ -5,6 +5,7 @@ import ( "sort" "testing" + "github.com/onflow/cadence/runtime" "github.com/onflow/cadence/runtime/common" "github.com/onflow/cadence/runtime/interpreter" coreContracts "github.com/onflow/flow-core-contracts/lib/go/contracts" @@ -14,17 +15,20 @@ import ( "github.com/stretchr/testify/require" "github.com/onflow/flow-go/cmd/util/ledger/util/registers" + "github.com/onflow/flow-go/fvm" "github.com/onflow/flow-go/fvm/environment" + "github.com/onflow/flow-go/fvm/storage/snapshot" "github.com/onflow/flow-go/fvm/systemcontracts" + "github.com/onflow/flow-go/ledger/common/convert" "github.com/onflow/flow-go/model/flow" ) -func oldExampleTokenCode(fungibleTokenAddress flow.Address) string { +func oldExampleFungibleTokenCode(fungibleTokenAddress flow.Address) string { return fmt.Sprintf( ` import FungibleToken from 0x%s - pub contract ExampleToken: FungibleToken { + pub contract ExampleFungibleToken: FungibleToken { pub var totalSupply: UFix64 pub resource Vault: FungibleToken.Provider, FungibleToken.Receiver, FungibleToken.Balance { @@ -68,6 +72,40 @@ func oldExampleTokenCode(fungibleTokenAddress flow.Address) string { ) } +func oldExampleNonFungibleTokenCode(fungibleTokenAddress flow.Address) string { + return fmt.Sprintf( + ` + import NonFungibleToken from 0x%s + + pub contract ExampleNFT: NonFungibleToken { + + /// Total supply of ExampleNFTs in existence + pub var totalSupply: UInt64 + + /// The core resource that represents a Non Fungible Token. + /// New instances will be created using the NFTMinter resource + /// and stored in the Collection resource + /// + pub resource NFT: NonFungibleToken.INFT { + + /// The unique ID that each NFT has + pub let id: UInt64 + + init(id: UInt64) { + self.id = id + } + } + + init() { + // Initialize the total supply + self.totalSupply = 0 + } + } + `, + fungibleTokenAddress.Hex(), + ) +} + func TestContractCheckingMigrationProgramRecovery(t *testing.T) { t.Parallel() @@ -77,6 +115,7 @@ func TestContractCheckingMigrationProgramRecovery(t *testing.T) { // Set up contracts const chainID = flow.Testnet + chain := chainID.Chain() systemContracts := systemcontracts.SystemContractsForChain(chainID) @@ -114,15 +153,30 @@ func TestContractCheckingMigrationProgramRecovery(t *testing.T) { systemContracts.FungibleToken, coreContracts.FungibleToken(env), ) + addSystemContract( + systemContracts.NonFungibleToken, + coreContracts.NonFungibleToken(env), + ) - // Use an old version of the ExampleToken contract, + const exampleFungibleTokenContractName = "ExampleFungibleToken" + const exampleNonFungibleTokenContractName = "ExampleNonFungibleToken" + + // Use an old version of the ExampleFungibleToken contract, + // and "deploy" it at some arbitrary, high (i.e. non-system) address + exampleAddress, err := chain.AddressAtIndex(1000) + require.NoError(t, err) + addContract( + exampleAddress, + exampleFungibleTokenContractName, + []byte(oldExampleFungibleTokenCode(systemContracts.FungibleToken.Address)), + ) + // Use an old version of the ExampleNonFungibleToken contract, // and "deploy" it at some arbitrary, high (i.e. non-system) address - exampleTokenAddress, err := chainID.Chain().AddressAtIndex(1000) require.NoError(t, err) addContract( - exampleTokenAddress, - "ExampleToken", - []byte(oldExampleTokenCode(systemContracts.FungibleToken.Address)), + exampleAddress, + exampleNonFungibleTokenContractName, + []byte(oldExampleNonFungibleTokenCode(systemContracts.NonFungibleToken.Address)), ) for address, addressContracts := range contracts { @@ -177,6 +231,11 @@ func TestContractCheckingMigrationProgramRecovery(t *testing.T) { assert.Equal(t, []any{ + contractCheckingSuccess{ + AccountAddress: common.Address(systemContracts.NonFungibleToken.Address), + ContractName: systemcontracts.ContractNameNonFungibleToken, + Code: string(coreContracts.NonFungibleToken(env)), + }, contractCheckingSuccess{ AccountAddress: common.Address(systemContracts.ViewResolver.Address), ContractName: systemcontracts.ContractNameViewResolver, @@ -193,11 +252,204 @@ func TestContractCheckingMigrationProgramRecovery(t *testing.T) { Code: string(coreContracts.FungibleToken(env)), }, contractCheckingSuccess{ - AccountAddress: common.Address(exampleTokenAddress), - ContractName: "ExampleToken", - Code: oldExampleTokenCode(systemContracts.FungibleToken.Address), + AccountAddress: common.Address(exampleAddress), + ContractName: exampleFungibleTokenContractName, + Code: oldExampleFungibleTokenCode(systemContracts.FungibleToken.Address), + }, + contractCheckingSuccess{ + AccountAddress: common.Address(exampleAddress), + ContractName: exampleNonFungibleTokenContractName, + Code: oldExampleNonFungibleTokenCode(systemContracts.NonFungibleToken.Address), }, }, reporter.entries, ) + + // Check that the programs are recovered correctly after the migration. + + mr, err := NewInterpreterMigrationRuntime(registersByAccount, chainID, InterpreterMigrationRuntimeConfig{}) + require.NoError(t, err) + + // First, we need to create the example account + + err = mr.Accounts.Create(nil, exampleAddress) + require.NoError(t, err) + + expectedAddresses := map[flow.Address]struct{}{ + exampleAddress: {}, + } + + err = mr.Commit(expectedAddresses, log) + require.NoError(t, err) + + // Next, we need to manually store contract values in the example account, + // simulating the effect of the deploying the original contracts. + // + // We need to do so with a new runtime, + // because the previous runtime's transaction state is finalized. + + mr, err = NewInterpreterMigrationRuntime(registersByAccount, chainID, InterpreterMigrationRuntimeConfig{}) + require.NoError(t, err) + + contractsStorageMap := mr.Storage.GetStorageMap( + common.Address(exampleAddress), + runtime.StorageDomainContract, + true, + ) + + inter := mr.Interpreter + + exampleFungibleTokenContractValue := interpreter.NewCompositeValue( + inter, + interpreter.EmptyLocationRange, + common.NewAddressLocation( + nil, + common.Address(exampleAddress), + exampleFungibleTokenContractName, + ), + exampleFungibleTokenContractName, + common.CompositeKindContract, + []interpreter.CompositeField{ + { + Name: "totalSupply", + Value: interpreter.NewUnmeteredUFix64ValueWithInteger(42, interpreter.EmptyLocationRange), + }, + }, + common.Address(exampleAddress), + ) + + contractsStorageMap.SetValue( + inter, + interpreter.StringStorageMapKey(exampleFungibleTokenContractName), + exampleFungibleTokenContractValue, + ) + + exampleNonFungibleTokenContractValue := interpreter.NewCompositeValue( + inter, + interpreter.EmptyLocationRange, + common.NewAddressLocation( + nil, + common.Address(exampleAddress), + exampleNonFungibleTokenContractName, + ), + exampleNonFungibleTokenContractName, + common.CompositeKindContract, + []interpreter.CompositeField{ + { + Name: "totalSupply", + Value: interpreter.NewUnmeteredUInt64Value(42), + }, + }, + common.Address(exampleAddress), + ) + + contractsStorageMap.SetValue( + inter, + interpreter.StringStorageMapKey(exampleNonFungibleTokenContractName), + exampleNonFungibleTokenContractValue, + ) + + err = mr.Storage.NondeterministicCommit(inter, false) + require.NoError(t, err) + + err = mr.Commit(expectedAddresses, log) + require.NoError(t, err) + + // Setup complete, now we can run the test transactions + + type testCase struct { + name string + code string + check func(t *testing.T, err error) + } + + testCases := []testCase{ + { + name: exampleFungibleTokenContractName, + code: fmt.Sprintf( + ` + import ExampleFungibleToken from %s + + transaction { + execute { + assert(ExampleFungibleToken.totalSupply == 42.0) + destroy ExampleFungibleToken.createEmptyVault( + vaultType: Type<@ExampleFungibleToken.Vault>() + ) + } + } + `, + exampleAddress.HexWithPrefix(), + ), + check: func(t *testing.T, err error) { + require.Error(t, err) + require.ErrorContains(t, err, "Contract ExampleFungibleToken is no longer functional") + require.ErrorContains(t, err, "createEmptyVault is not available in recovered program.") + }, + }, + { + name: exampleNonFungibleTokenContractName, + code: fmt.Sprintf( + ` + import ExampleNonFungibleToken from %s + + transaction { + execute { + destroy ExampleNonFungibleToken.createEmptyCollection( + nftType: Type<@ExampleNonFungibleToken.NFT>() + ) + } + } + `, + exampleAddress.HexWithPrefix(), + ), + check: func(t *testing.T, err error) { + require.Error(t, err) + require.ErrorContains(t, err, "Contract ExampleNonFungibleToken is no longer functional") + require.ErrorContains(t, err, "createEmptyCollection is not available in recovered program.") + }, + }, + } + + storageSnapshot := snapshot.MapStorageSnapshot{} + + newPayloads := registersByAccount.DestructIntoPayloads(1) + + for _, newPayload := range newPayloads { + registerID, registerValue, err := convert.PayloadToRegister(newPayload) + require.NoError(t, err) + + storageSnapshot[registerID] = registerValue + } + + test := func(testCase testCase) { + + t.Run(testCase.name, func(t *testing.T) { + + txBody := flow.NewTransactionBody(). + SetScript([]byte(testCase.code)) + + vm := fvm.NewVirtualMachine() + + ctx := fvm.NewContext( + fvm.WithChain(chain), + fvm.WithAuthorizationChecksEnabled(false), + fvm.WithSequenceNumberCheckAndIncrementEnabled(false), + fvm.WithCadenceLogging(true), + ) + + _, output, err := vm.Run( + ctx, + fvm.Transaction(txBody, 0), + storageSnapshot, + ) + + require.NoError(t, err) + testCase.check(t, output.Err) + }) + } + + for _, testCase := range testCases { + test(testCase) + } } diff --git a/cmd/util/ledger/migrations/fix_broken_data_migration.go b/cmd/util/ledger/migrations/fix_broken_data_migration.go index e5b489ffda8..81988dddddd 100644 --- a/cmd/util/ledger/migrations/fix_broken_data_migration.go +++ b/cmd/util/ledger/migrations/fix_broken_data_migration.go @@ -122,25 +122,15 @@ func (m *FixSlabsWithBrokenReferencesMigration) MigrateAccount( return fmt.Errorf("failed to commit storage: %w", err) } - // Finalize the transaction - result, err := migrationRuntime.TransactionState.FinalizeMainTransaction() - if err != nil { - return fmt.Errorf("failed to finalize main transaction: %w", err) - } + // Commit/finalize the transaction - // Merge the changes to the original payloads. expectedAddresses := map[flow.Address]struct{}{ flow.Address(address): {}, } - err = registers.ApplyChanges( - accountRegisters, - result.WriteSet, - expectedAddresses, - m.log, - ) + err = migrationRuntime.Commit(expectedAddresses, m.log) if err != nil { - return fmt.Errorf("failed to apply changes to account registers: %w", err) + return fmt.Errorf("failed to commit: %w", err) } // Log fixed payloads diff --git a/cmd/util/ledger/migrations/migrator_runtime.go b/cmd/util/ledger/migrations/migrator_runtime.go index ba72224d520..72a049539ec 100644 --- a/cmd/util/ledger/migrations/migrator_runtime.go +++ b/cmd/util/ledger/migrations/migrator_runtime.go @@ -8,6 +8,7 @@ import ( "github.com/onflow/cadence/runtime/interpreter" "github.com/onflow/cadence/runtime/stdlib" "github.com/onflow/crypto/hash" + "github.com/rs/zerolog" "github.com/onflow/flow-go/cmd/util/ledger/util" "github.com/onflow/flow-go/cmd/util/ledger/util/registers" @@ -19,12 +20,32 @@ import ( ) type BasicMigrationRuntime struct { + Registers registers.Registers TransactionState state.NestedTransactionPreparer Storage *runtime.Storage AccountsLedger *util.AccountsAtreeLedger Accounts environment.Accounts } +func (r *BasicMigrationRuntime) Commit(expectedAddresses map[flow.Address]struct{}, log zerolog.Logger) error { + + result, err := r.TransactionState.FinalizeMainTransaction() + if err != nil { + return fmt.Errorf("failed to finalize main transaction: %w", err) + } + + err = registers.ApplyChanges( + r.Registers, + result.WriteSet, + expectedAddresses, + log, + ) + if err != nil { + return fmt.Errorf("failed to apply changes: %w", err) + } + return nil +} + type InterpreterMigrationRuntime struct { *BasicMigrationRuntime Interpreter *interpreter.Interpreter @@ -120,6 +141,7 @@ func NewBasicMigrationRuntime(regs registers.Registers) *BasicMigrationRuntime { runtimeStorage := runtime.NewStorage(accountsAtreeLedger, nil) return &BasicMigrationRuntime{ + Registers: regs, TransactionState: transactionState, Storage: runtimeStorage, AccountsLedger: accountsAtreeLedger, diff --git a/cmd/util/ledger/util/migration_runtime_interface.go b/cmd/util/ledger/util/migration_runtime_interface.go index 7db157f860f..8a43d7cff3b 100644 --- a/cmd/util/ledger/util/migration_runtime_interface.go +++ b/cmd/util/ledger/util/migration_runtime_interface.go @@ -62,8 +62,6 @@ func NewMigrationRuntimeInterface( } } -var _ runtime.Interface = &MigrationRuntimeInterface{} - func (m *MigrationRuntimeInterface) ResolveLocation( identifiers []runtime.Identifier, location runtime.Location, diff --git a/fvm/environment/program_recovery.go b/fvm/environment/program_recovery.go index 439d6a46d02..c8946e9a4ec 100644 --- a/fvm/environment/program_recovery.go +++ b/fvm/environment/program_recovery.go @@ -27,24 +27,34 @@ func RecoverProgram( sc := systemcontracts.SystemContractsForChain(chainID) fungibleTokenAddress := common.Address(sc.FungibleToken.Address) + nonFungibleTokenAddress := common.Address(sc.NonFungibleToken.Address) - if !isFungibleTokenContract(program, fungibleTokenAddress) { - return nil, nil - } + switch { + case isFungibleTokenContract(program, fungibleTokenAddress): + return RecoveredFungibleTokenCode(fungibleTokenAddress, addressLocation.Name), nil - contractName := addressLocation.Name + case isNonFungibleTokenContract(program, nonFungibleTokenAddress): + return RecoveredNonFungibleTokenCode(nonFungibleTokenAddress, addressLocation.Name), nil + } - return []byte(RecoveredFungibleTokenCode(fungibleTokenAddress, contractName)), nil + return nil, nil } -func RecoveredFungibleTokenCode(fungibleTokenAddress common.Address, contractName string) string { - return fmt.Sprintf( +func RecoveredFungibleTokenCode(fungibleTokenAddress common.Address, contractName string) []byte { + return []byte(fmt.Sprintf( //language=Cadence ` - import FungibleToken from %s + import FungibleToken from %[1]s access(all) - contract %s: FungibleToken { + contract %[2]s: FungibleToken { + + access(self) + view fun recoveryPanic(_ functionName: String): Never { + return panic( + "%[3]s ".concat(functionName).concat(" is not available in recovered program.") + ) + } access(all) var totalSupply: UFix64 @@ -55,12 +65,17 @@ func RecoveredFungibleTokenCode(fungibleTokenAddress common.Address, contractNam access(all) view fun getContractViews(resourceType: Type?): [Type] { - panic("getContractViews is not implemented") + %[2]s.recoveryPanic("getContractViews") } access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? { - panic("resolveContractView is not implemented") + %[2]s.recoveryPanic("resolveContractView") + } + + access(all) + fun createEmptyVault(vaultType: Type): @{FungibleToken.Vault} { + %[2]s.recoveryPanic("createEmptyVault") } access(all) @@ -75,44 +90,110 @@ func RecoveredFungibleTokenCode(fungibleTokenAddress common.Address, contractNam access(FungibleToken.Withdraw) fun withdraw(amount: UFix64): @{FungibleToken.Vault} { - panic("withdraw is not implemented") + %[2]s.recoveryPanic("Vault.withdraw") } access(all) view fun isAvailableToWithdraw(amount: UFix64): Bool { - panic("isAvailableToWithdraw is not implemented") + %[2]s.recoveryPanic("Vault.isAvailableToWithdraw") } access(all) fun deposit(from: @{FungibleToken.Vault}) { - panic("deposit is not implemented") + %[2]s.recoveryPanic("Vault.deposit") } access(all) fun createEmptyVault(): @{FungibleToken.Vault} { - panic("createEmptyVault is not implemented") + %[2]s.recoveryPanic("Vault.createEmptyVault") } access(all) view fun getViews(): [Type] { - panic("getViews is not implemented") + %[2]s.recoveryPanic("Vault.getViews") } access(all) fun resolveView(_ view: Type): AnyStruct? { - panic("resolveView is not implemented") + %[2]s.recoveryPanic("Vault.resolveView") } } + } + `, + fungibleTokenAddress.HexWithPrefix(), + contractName, + fmt.Sprintf("Contract %s is no longer functional. "+ + "A version of the contract has been recovered to allow access to the fields declared in the FT standard.", + contractName, + ), + )) +} + +func RecoveredNonFungibleTokenCode(nonFungibleTokenAddress common.Address, contractName string) []byte { + return []byte(fmt.Sprintf( + //language=Cadence + ` + import NonFungibleToken from %[1]s + + access(all) + contract %[2]s: NonFungibleToken { + + access(self) + view fun recoveryPanic(_ functionName: String): Never { + return panic( + "%[3]s ".concat(functionName).concat(" is not available in recovered program.") + ) + } access(all) - fun createEmptyVault(vaultType: Type): @{FungibleToken.Vault} { - panic("createEmptyVault is not implemented") + view fun getContractViews(resourceType: Type?): [Type] { + %[2]s.recoveryPanic("getContractViews") + } + + access(all) + fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? { + %[2]s.recoveryPanic("resolveContractView") + } + + access(all) + fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection} { + %[2]s.recoveryPanic("createEmptyCollection") + } + + access(all) + resource NFT: NonFungibleToken.NFT { + + access(all) + let id: UInt64 + + init(id: UInt64) { + self.id = id + } + + access(all) + view fun getViews(): [Type] { + %[2]s.recoveryPanic("NFT.getViews") + } + + access(all) + fun resolveView(_ view: Type): AnyStruct? { + %[2]s.recoveryPanic("NFT.resolveView") + } + + access(all) + fun createEmptyCollection(): @{NonFungibleToken.Collection} { + %[2]s.recoveryPanic("NFT.createEmptyCollection") + } } } `, - fungibleTokenAddress.HexWithPrefix(), + nonFungibleTokenAddress.HexWithPrefix(), contractName, - ) + fmt.Sprintf("Contract %s is no longer functional. "+ + "A version of the contract has been recovered to allow access to the fields declared in the NFT standard.", + contractName, + ), + )) } func importsAddressLocation(program *ast.Program, address common.Address, name string) bool { @@ -165,6 +246,11 @@ const fungibleTokenTypeTotalSupplyFieldName = "totalSupply" const fungibleTokenVaultTypeIdentifier = "Vault" const fungibleTokenVaultTypeBalanceFieldName = "balance" +const nonFungibleTokenTypeIdentifier = "NonFungibleToken" +const nonFungibleTokenTypeTotalSupplyFieldName = "totalSupply" +const nonFungibleTokenNFTTypeIdentifier = "NFT" +const nonFungibleTokenNFTTypeIDFieldName = "id" + func isFungibleTokenContract(program *ast.Program, fungibleTokenAddress common.Address) bool { // Check if the contract imports the FungibleToken contract @@ -214,6 +300,54 @@ func isFungibleTokenContract(program *ast.Program, fungibleTokenAddress common.A return true } +func isNonFungibleTokenContract(program *ast.Program, nonFungibleTokenAddress common.Address) bool { + + // Check if the contract imports the NonFungibleToken contract + if !importsAddressLocation(program, nonFungibleTokenAddress, nonFungibleTokenTypeIdentifier) { + return false + } + + contractDeclaration := program.SoleContractDeclaration() + if contractDeclaration == nil { + return false + } + + // Check if the contract implements the NonFungibleToken interface + if !declaresConformanceTo(contractDeclaration, nonFungibleTokenTypeIdentifier) { + return false + } + + // Check if the contract has a totalSupply field + totalSupplyFieldDeclaration := getField(contractDeclaration, nonFungibleTokenTypeTotalSupplyFieldName) + if totalSupplyFieldDeclaration == nil { + return false + } + + // Check if the totalSupply field is of type UInt64 + if !isNominalType(totalSupplyFieldDeclaration.TypeAnnotation.Type, sema.UInt64TypeName) { + return false + } + + // Check if the contract has an NFT resource + nftDeclaration := contractDeclaration.Members.CompositesByIdentifier()[nonFungibleTokenNFTTypeIdentifier] + if nftDeclaration == nil { + return false + } + + // Check if the NFT resource has an id field + idFieldDeclaration := getField(nftDeclaration, nonFungibleTokenNFTTypeIDFieldName) + if idFieldDeclaration == nil { + return false + } + + // Check if the id field is of type UInt64 + if !isNominalType(idFieldDeclaration.TypeAnnotation.Type, sema.UInt64TypeName) { + return false + } + + return true +} + func getField(declaration *ast.CompositeDeclaration, name string) *ast.FieldDeclaration { for _, fieldDeclaration := range declaration.Members.Fields() { if fieldDeclaration.Identifier.Identifier == name {