diff --git a/registry/cadence/README.md b/registry/cadence/README.md new file mode 100644 index 0000000..7704d5c --- /dev/null +++ b/registry/cadence/README.md @@ -0,0 +1,6 @@ +### 👋 Welcome Flix Developer! + +To run the tests execute the following command: +```bash +flow test --cover --covercode="contracts" cadence/test/flix_registry_test.cdc +``` diff --git a/registry/cadence/contracts/flix_registry.cdc b/registry/cadence/contracts/flix_registry.cdc new file mode 100644 index 0000000..fec4d41 --- /dev/null +++ b/registry/cadence/contracts/flix_registry.cdc @@ -0,0 +1,126 @@ +import "FLIXRegistryInterface" + +access(all) +contract FLIXRegistry: FLIXRegistryInterface { + + access(all) + event ContractInitialized() + + access(all) + event RegistryCreated(name: String) + + access(all) + event Published(registryOwner: Address, registryName: String, registryUuid: UInt64, alias: String, id: String, cadenceBodyHash: String) + + access(all) + event Removed(registryOwner: Address, registryName: String, registryUuid: UInt64, id: String) + + access(all) + event Deprecated(registryOwner: Address, registryName: String, registryUuid: UInt64, id: String) + + access(all) + event AliasLinked(registryOwner: Address, registryName: String, registryUuid: UInt64, alias: String, id: String) + + access(all) + event AliasUnlinked(registryOwner: Address, registryName: String, registryUuid: UInt64, alias: String) + + access(all) + resource Registry: FLIXRegistryInterface.Queryable, FLIXRegistryInterface.Removable, FLIXRegistryInterface.Admin { + + access(account) var flixes: {String: {FLIXRegistryInterface.InteractionTemplate}} + access(account) var aliases: {String: String} + access(account) var cadenceBodyHashes: {String: {FLIXRegistryInterface.InteractionTemplate}} + access(account) let name: String + + access(all) + fun getName(): String { + return self.name + } + + access(all) + fun publish(alias: String, flix: {FLIXRegistryInterface.InteractionTemplate}) { + self.flixes[flix.getId()] = flix + self.aliases[alias] = flix.getId() + self.cadenceBodyHashes[flix.getCadenceBodyHash()] = flix + emit Published(registryOwner: self.owner!.address, registryName: self.name, registryUuid: self.uuid, alias: alias, id: flix.getId(), cadenceBodyHash: flix.getCadenceBodyHash()) + } + + access(all) + fun link(alias: String, id: String) { + self.aliases[alias] = id + emit AliasLinked(registryOwner: self.owner!.address, registryName: self.name, registryUuid: self.uuid, alias: alias, id: id) + } + + access(all) + fun unlink(alias: String) { + self.aliases.remove(key: alias) + emit AliasUnlinked(registryOwner: self.owner!.address, registryName: self.name, registryUuid: self.uuid, alias: alias) + } + + access(all) + fun lookup(idOrAlias: String): {FLIXRegistryInterface.InteractionTemplate}? { + if self.aliases.containsKey(idOrAlias) { + return self.flixes[self.aliases[idOrAlias]!] + } + return self.flixes[idOrAlias] + } + + access(all) + fun resolve(cadenceBodyHash: String): {FLIXRegistryInterface.InteractionTemplate}? { + return self.cadenceBodyHashes[cadenceBodyHash] + } + + access(all) + fun deprecate(idOrAlias: String) { + var flix = self.lookup(idOrAlias: idOrAlias) ?? panic("FLIX does not exist with the given id or alias: ".concat(idOrAlias)) + flix.status = "deprecated" + self.flixes[flix.getId()] = flix + emit Deprecated(registryOwner: self.owner!.address, registryName: self.name, registryUuid: self.uuid, id: flix.getId()) + } + + access(all) + fun remove(id: String): {FLIXRegistryInterface.InteractionTemplate}? { + let removed = self.flixes.remove(key: id) + if(removed != nil) { emit Removed(registryOwner: self.owner!.address, registryName: self.name, registryUuid: self.uuid, id: id) } + return removed + } + + access(all) + fun getIds(): [String] { + return self.flixes.keys + } + + access(all) + fun getAllAlias(): {String: String} { + return self.aliases + } + + init(name: String) { + self.name = name + self.flixes = {} + self.aliases = {} + self.cadenceBodyHashes = {} + emit RegistryCreated(name: name) + } + } + + access(all) + fun createRegistry(name: String): @Registry { + return <- create Registry(name: name) + } + + access(all) + fun PublicPath(name: String): PublicPath { + return PublicPath(identifier: "flix_".concat(name))! + } + + access(all) + fun StoragePath(name: String): StoragePath { + return StoragePath(identifier: "flix_".concat(name))! + } + + init() { + emit ContractInitialized() + } + +} diff --git a/registry/cadence/contracts/flix_registry_interface.cdc b/registry/cadence/contracts/flix_registry_interface.cdc new file mode 100644 index 0000000..fad23be --- /dev/null +++ b/registry/cadence/contracts/flix_registry_interface.cdc @@ -0,0 +1,93 @@ +access(all) +contract interface FLIXRegistryInterface { + + access(all) + event ContractInitialized() + + access(all) + event RegistryCreated(name: String) + + access(all) + event Published(registryOwner: Address, registryName: String, registryUuid: UInt64, alias: String, id: String, cadenceBodyHash: String) + + access(all) + event Removed(registryOwner: Address, registryName: String, registryUuid: UInt64, id: String) + + access(all) + event Deprecated(registryOwner: Address, registryName: String, registryUuid: UInt64, id: String) + + access(all) + event AliasLinked(registryOwner: Address, registryName: String, registryUuid: UInt64, alias: String, id: String) + + access(all) + event AliasUnlinked(registryOwner: Address, registryName: String, registryUuid: UInt64, alias: String) + + access(all) + struct interface InteractionTemplate { + access(all) + fun getId(): String + + access(all) + fun getVersion(): String + + access(all) + fun getCadenceBodyHash(): String + + pub(set) + var status: String + + access(all) + fun getData(): AnyStruct + } + + access(all) + resource interface Queryable{ + access(all) + fun getName(): String + + access(all) + fun resolve(cadenceBodyHash: String): {FLIXRegistryInterface.InteractionTemplate}? + + access(all) + fun lookup(idOrAlias: String): {FLIXRegistryInterface.InteractionTemplate}? + + access(all) + fun getIds(): [String] + + access(all) + fun getAllAlias(): {String: String} + } + + access(all) + resource interface Admin { + + // Add the flix to the registry and add or update the alias to point to this flix + access(all) + fun publish(alias: String, flix: {FLIXRegistryInterface.InteractionTemplate}) + + access(all) + fun link(alias: String, id: String) + + access(all) + fun unlink(alias: String) + + access(all) + fun deprecate(idOrAlias: String) + } + + access(all) + resource interface Removable { + access(all) + fun remove(id: String): AnyStruct? + } + + access(all) + resource Registry: Queryable, Removable, Admin { + access(all) + fun getName(): String + } + + access(all) + fun createRegistry(name: String): @Registry + +} \ No newline at end of file diff --git a/registry/cadence/contracts/flix_schema_draft.cdc b/registry/cadence/contracts/flix_schema_draft.cdc new file mode 100644 index 0000000..210329c --- /dev/null +++ b/registry/cadence/contracts/flix_schema_draft.cdc @@ -0,0 +1,49 @@ +import "FLIXRegistryInterface" +import "FLIXRegistry" + +access(all) +contract FLIXSchema_draft { + + access(all) + struct FLIX: FLIXRegistryInterface.InteractionTemplate { + access(all) + let id: String + + access(all) + let data: AnyStruct + + access(all) + let cadenceBodyHash: String + + access(all) + fun getId(): String { + return self.id + } + + access(all) + fun getData(): AnyStruct { + return self.data + } + + access(all) + fun getVersion(): String { + return "draft" + } + + access(all) + fun getCadenceBodyHash(): String { + return self.cadenceBodyHash + } + + pub(set) + var status: String + + init(id: String, data: AnyStruct, cadenceBodyHash: String) { + self.id = id + self.data = data + self.cadenceBodyHash = cadenceBodyHash + self.status = "active" + } + } + +} \ No newline at end of file diff --git a/registry/cadence/contracts/flix_schema_v1_1_0.cdc b/registry/cadence/contracts/flix_schema_v1_1_0.cdc new file mode 100644 index 0000000..b7c2ee1 --- /dev/null +++ b/registry/cadence/contracts/flix_schema_v1_1_0.cdc @@ -0,0 +1,202 @@ +import "FLIXRegistryInterface" +import "FLIXRegistry" + +access(all) +contract FLIXSchema_v1_1_0 { + + access(all) + struct FLIX: FLIXRegistryInterface.InteractionTemplate { + access(all) + let id: String + + access(all) + let data: Data + + access(all) + let cadenceBodyHash: String + + access(all) + fun getId(): String { + return self.id + } + + access(all) + fun getData(): Data { + return self.data + } + + access(all) + fun getVersion(): String { + return "1.1.0" + } + + access(all) + fun getCadenceBodyHash(): String { + return self.cadenceBodyHash + } + + access(all) + fun getStatus(): String { + return self.status + } + + pub(set) + var status: String + + init(id: String, data: Data, cadenceBodyHash: String) { + self.id = id + self.data = data + self.cadenceBodyHash = cadenceBodyHash + self.status = "active" + } + } + + access(all) struct Data { + access(all) var type: String + access(all) var interface: String + access(all) var messages: [Message] + access(all) var cadence: Cadence + access(all) var dependencies: [Dependency] + access(all) var parameters: [Parameter] + + init(type: String, interface: String, messages: [Message], cadence: Cadence, dependencies: [Dependency], parameters: [Parameter]) { + self.type = type + self.interface = interface + self.messages = messages + self.cadence = cadence + self.dependencies = dependencies + self.parameters = parameters + } + } + + access(all) struct Message { + access(all) var key: String + access(all) var i18n: [I18n] + + init(key: String, i18n: [I18n]) { + self.key = key + self.i18n = i18n + } + } + + access(all) struct I18n { + access(all) var tag: String + access(all) var translation: String + + init(tag: String, translation: String) { + self.tag = tag + self.translation = translation + } + } + + access(all) struct Cadence { + access(all) var body: String + access(all) var networkPins: [NetworkPin] + + init(body: String, networkPins: [NetworkPin]) { + self.body = body + self.networkPins = networkPins + } + } + + access(all) struct NetworkPin { + access(all) var network: String + access(all) var pinSelf: String + + init(network: String, pinSelf: String) { + self.network = network + self.pinSelf = pinSelf + } + } + + access(all) struct Dependency { + access(all) var contracts: [Contract] + + init(contracts: [Contract]) { + self.contracts = contracts + } + } + + access(all) struct Contract { + access(all) var contract: String + access(all) var networks: [Network] + + init(contract: String, networks: [Network]) { + self.contract = contract + self.networks = networks + } + } + + access(all) struct Network { + access(all) var network: String + access(all) var address: String + access(all) var dependencyPinBlockHeight: UInt64 + access(all) var dependencyPin: DependencyPin + + init(network: String, address: String, dependencyPinBlockHeight: UInt64, dependencyPin: DependencyPin) { + self.network = network + self.address = address + self.dependencyPinBlockHeight = dependencyPinBlockHeight + self.dependencyPin = dependencyPin + } + } + + access(all) struct DependencyPin { + access(all) var pin: String + access(all) var pinSelf: String + access(all) var pinContractName: String + access(all) var pinContractAddress: String + access(all) var imports: [Import] + + init(pin: String, pinSelf: String, pinContractName: String, pinContractAddress: String, imports: [Import]) { + self.pin = pin + self.pinSelf = pinSelf + self.pinContractName = pinContractName + self.pinContractAddress = pinContractAddress + self.imports = imports + } + } + + access(all) struct Import { + access(all) var pin: String + access(all) var pinSelf: String + access(all) var pinContractName: String + access(all) var pinContractAddress: String + access(all) var imports: [Import] + + init(pin: String, pinSelf: String, pinContractName: String, pinContractAddress: String, imports: [Import]) { + self.pin = pin + self.pinSelf = pinSelf + self.pinContractName = pinContractName + self.pinContractAddress = pinContractAddress + self.imports = imports + } + } + + access(all) struct Parameter { + access(all) var label: String + access(all) var index: Int + access(all) var type: String + access(all) var messages: [Message] + access(all) var balance: [Balance] + + init(label: String, index: Int, type: String, messages: [Message], balance: [Balance]) { + self.label = label + self.index = index + self.type = type + self.messages = messages + self.balance = balance + } + } + + access(all) struct Balance { + access(all) var network: String + access(all) var pin: String + + init(network: String, pin: String) { + self.network = network + self.pin = pin + } + } + +} \ No newline at end of file diff --git a/registry/cadence/scripts/get_all_alias.cdc b/registry/cadence/scripts/get_all_alias.cdc new file mode 100644 index 0000000..8fcf32c --- /dev/null +++ b/registry/cadence/scripts/get_all_alias.cdc @@ -0,0 +1,11 @@ +import "FLIXRegistry" +import "FLIXRegistryInterface" + +pub fun main(accountAddress: Address, registryName: String): {String: String} { + let account = getAccount(accountAddress) + let registry = account.getCapability(FLIXRegistry.PublicPath(name: registryName)) + .borrow<&FLIXRegistry.Registry{FLIXRegistryInterface.Queryable}>() + ?? panic("Could not borrow a reference to the Registry") + + return registry.getAllAlias() +} \ No newline at end of file diff --git a/registry/cadence/scripts/get_registry_size.cdc b/registry/cadence/scripts/get_registry_size.cdc new file mode 100644 index 0000000..24873a5 --- /dev/null +++ b/registry/cadence/scripts/get_registry_size.cdc @@ -0,0 +1,13 @@ +import "FLIXRegistry" +import "FLIXRegistryInterface" + +pub fun main(address: Address, registryName: String): Int { + let account = getAccount(address) + let registry = account.getCapability(FLIXRegistry.PublicPath(name: registryName)) + .borrow<&FLIXRegistry.Registry{FLIXRegistryInterface.Queryable}>() + ?? panic("Could not borrow a reference to the Registry") + + assert(registryName == registry.getName()) + + return registry.getIds().length +} \ No newline at end of file diff --git a/registry/cadence/scripts/lookup.cdc b/registry/cadence/scripts/lookup.cdc new file mode 100644 index 0000000..70ad998 --- /dev/null +++ b/registry/cadence/scripts/lookup.cdc @@ -0,0 +1,11 @@ +import "FLIXRegistry" +import "FLIXRegistryInterface" + +pub fun main(accountAddress: Address, idOrAlias: String, registryName: String): AnyStruct{FLIXRegistryInterface.InteractionTemplate}? { + let account = getAccount(accountAddress) + let registry = account.getCapability(FLIXRegistry.PublicPath(name: registryName)) + .borrow<&FLIXRegistry.Registry{FLIXRegistryInterface.Queryable}>() + ?? panic("Could not borrow a reference to the Registry") + + return registry.lookup(idOrAlias: idOrAlias) +} \ No newline at end of file diff --git a/registry/cadence/scripts/resolve.cdc b/registry/cadence/scripts/resolve.cdc new file mode 100644 index 0000000..f0215c0 --- /dev/null +++ b/registry/cadence/scripts/resolve.cdc @@ -0,0 +1,11 @@ +import "FLIXRegistry" +import "FLIXRegistryInterface" + +pub fun main(accountAddress: Address, cadenceBodyHash: String, registryName: String): AnyStruct{FLIXRegistryInterface.InteractionTemplate}? { + let account = getAccount(accountAddress) + let registry = account.getCapability(FLIXRegistry.PublicPath(name: registryName)) + .borrow<&FLIXRegistry.Registry{FLIXRegistryInterface.Queryable}>() + ?? panic("Could not borrow a reference to the Registry") + + return registry.resolve(cadenceBodyHash: cadenceBodyHash) +} \ No newline at end of file diff --git a/registry/cadence/test/flix_registry_test.cdc b/registry/cadence/test/flix_registry_test.cdc new file mode 100644 index 0000000..4be811c --- /dev/null +++ b/registry/cadence/test/flix_registry_test.cdc @@ -0,0 +1,338 @@ +import Test +import BlockchainHelpers +import "FLIXSchema_draft" +import "FLIXRegistry" + +access(all) let REGISTRY_OWNER = Test.createAccount() +access(all) let TEMPLATE_ID = "aTestId" +access(all) let ALIAS = "anAlias" +access(all) let NEW_ALIAS = "aNewAlias" +access(all) let REGISTRY_NAME = "someName" +access(all) let FLIX_DATA: AnyStruct = "same data" +access(all) let CADENCE_BODY_HASH = "someHash" + +access(all) let CONTRACT_DEPLOYED_SNAPSHOT = "contract deployed" +access(all) let REGISTRY_CREATED_SNAPSHOT = "registry created" +access(all) let FLIX_PUBLISHED_SNAPSHOT = "flix published" + + +access(all) +fun setup() { + + let flixRegistryInterfaceErr = Test.deployContract( + name: "FLIXRegistryInterface", + path: "../contracts/flix_registry_interface.cdc", + arguments: [] + ) + Test.expect(flixRegistryInterfaceErr, Test.beNil()) + + let flixRegistryErr = Test.deployContract( + name: "FLIXRegistry", + path: "../contracts/flix_registry.cdc", + arguments: [] + ) + Test.expect(flixRegistryErr, Test.beNil()) + + let flixSchema_draftErr = Test.deployContract( + name: "FLIXSchema_draft", + path: "../contracts/flix_schema_draft.cdc", + arguments: [] + ) + Test.expect(flixSchema_draftErr, Test.beNil()) + Test.createSnapshot(name: CONTRACT_DEPLOYED_SNAPSHOT) + + let createTxResult = executeTransaction( + "../transactions/create_registry.cdc", + [REGISTRY_NAME], + REGISTRY_OWNER + ) + Test.expect(createTxResult, Test.beSucceeded()) + Test.createSnapshot(name: REGISTRY_CREATED_SNAPSHOT) + + let publishTxResult = executeTransaction( + "../transactions/publish_flix.cdc", + [ALIAS, TEMPLATE_ID, FLIX_DATA, CADENCE_BODY_HASH, REGISTRY_NAME], + REGISTRY_OWNER + ) + Test.expect(publishTxResult, Test.beSucceeded()) + Test.createSnapshot(name: FLIX_PUBLISHED_SNAPSHOT) +} + +access(all) +fun testShouldEmitContractInitializedEvent() { + Test.loadSnapshot(name: REGISTRY_CREATED_SNAPSHOT) + + let typ = Type() + Test.assertEqual(1, Test.eventsOfType(typ).length) +} + +access(all) +fun testShouldEmitRegistryCreatedEvent() { + Test.loadSnapshot(name: REGISTRY_CREATED_SNAPSHOT) + + let typ = Type() + let events = Test.eventsOfType(typ) + let event = events[0] as! FLIXRegistry.RegistryCreated + Test.assertEqual(1, events.length) + Test.assertEqual(REGISTRY_NAME, event.name) +} + +access(all) +fun testShouldCreateRegistry() { + Test.loadSnapshot(name: REGISTRY_CREATED_SNAPSHOT) + + let scriptResult = executeScript( + "../scripts/get_registry_size.cdc", + [REGISTRY_OWNER.address, REGISTRY_NAME] + ) + Test.expect(scriptResult, Test.beSucceeded()) + + let registrySize = scriptResult.returnValue! as! Int + Test.assertEqual(0, registrySize) +} + +access(all) +fun testShouldEmitEventAfterFlixPublished() { + Test.loadSnapshot(name: FLIX_PUBLISHED_SNAPSHOT) + + let typ = Type() + let events = Test.eventsOfType(typ) + let event = events[0] as! FLIXRegistry.Published + Test.assertEqual(1, events.length) + Test.assertEqual(REGISTRY_NAME, event.registryName) + Test.assertEqual(REGISTRY_OWNER.address, event.registryOwner) + Test.assertEqual(ALIAS, event.alias) + Test.assertEqual(TEMPLATE_ID, event.id) + Test.assertEqual(CADENCE_BODY_HASH, event.cadenceBodyHash) +} + +access(all) +fun testShouldContainFlixAfterPublished() { + Test.loadSnapshot(name: FLIX_PUBLISHED_SNAPSHOT) + + let scriptResult = executeScript( + "../scripts/get_registry_size.cdc", + [REGISTRY_OWNER.address, REGISTRY_NAME] + ) + Test.expect(scriptResult, Test.beSucceeded()) + + let registrySize = scriptResult.returnValue! as! Int + Test.assertEqual(1, registrySize) +} + +access(all) +fun testShouldLookupFlixAfterPublished() { + Test.loadSnapshot(name: FLIX_PUBLISHED_SNAPSHOT) + + let lookupScriptResult = executeScript( + "../scripts/lookup.cdc", + [REGISTRY_OWNER.address, ALIAS, REGISTRY_NAME] + ) + + Test.expect(lookupScriptResult, Test.beSucceeded()) + + let flix = lookupScriptResult.returnValue! as! FLIXSchema_draft.FLIX + Test.assertEqual(TEMPLATE_ID, flix.id) + Test.assertEqual(FLIX_DATA, flix.getData()) + Test.assertEqual(CADENCE_BODY_HASH, flix.cadenceBodyHash) + Test.assertEqual("active", flix.status) + Test.assertEqual("draft", flix.getVersion()) +} + +access(all) +fun testShouldResolveFlixAfterPublished() { + Test.loadSnapshot(name: FLIX_PUBLISHED_SNAPSHOT) + + let resolveScriptResult = executeScript( + "../scripts/resolve.cdc", + [REGISTRY_OWNER.address, CADENCE_BODY_HASH, REGISTRY_NAME] + ) + + Test.expect(resolveScriptResult, Test.beSucceeded()) + + let resolvedFlix = resolveScriptResult.returnValue! as! FLIXSchema_draft.FLIX + Test.assertEqual(TEMPLATE_ID, resolvedFlix.id) + Test.assertEqual(FLIX_DATA, resolvedFlix.getData()) + Test.assertEqual(CADENCE_BODY_HASH, resolvedFlix.cadenceBodyHash) + Test.assertEqual("active", resolvedFlix.status) +} + +access(all) +fun testShouldLinkAlias() { + Test.loadSnapshot(name: FLIX_PUBLISHED_SNAPSHOT) + + let txResult = executeTransaction( + "../transactions/link_alias.cdc", + [NEW_ALIAS, TEMPLATE_ID, REGISTRY_NAME], + REGISTRY_OWNER + ) + Test.expect(txResult, Test.beSucceeded()) + + let typ = Type() + let events = Test.eventsOfType(typ) + let event = events[0] as! FLIXRegistry.AliasLinked + Test.assertEqual(1, events.length) + Test.assertEqual(REGISTRY_NAME, event.registryName) + Test.assertEqual(REGISTRY_OWNER.address, event.registryOwner) + Test.assertEqual(NEW_ALIAS, event.alias) + Test.assertEqual(TEMPLATE_ID, event.id) + + let scriptResult = executeScript( + "../scripts/get_all_alias.cdc", + [REGISTRY_OWNER.address, REGISTRY_NAME] + ) + Test.expect(scriptResult, Test.beSucceeded()) + + let aliases = scriptResult.returnValue! as! {String: String} + + Test.assertEqual(TEMPLATE_ID, aliases[ALIAS]!) + Test.assertEqual(TEMPLATE_ID, aliases[NEW_ALIAS]!) +} + +access(all) +fun testShouldUnlinkAlias() { + Test.loadSnapshot(name: FLIX_PUBLISHED_SNAPSHOT) + + let txResult = executeTransaction( + "../transactions/unlink_alias.cdc", + [ALIAS, REGISTRY_NAME], + REGISTRY_OWNER + ) + Test.expect(txResult, Test.beSucceeded()) + + let typ = Type() + let events = Test.eventsOfType(typ) + let event = events[0] as! FLIXRegistry.AliasUnlinked + Test.assertEqual(1, events.length) + Test.assertEqual(REGISTRY_NAME, event.registryName) + Test.assertEqual(REGISTRY_OWNER.address, event.registryOwner) + Test.assertEqual(ALIAS, event.alias) + + let scriptResult = executeScript( + "../scripts/get_all_alias.cdc", + [REGISTRY_OWNER.address, REGISTRY_NAME] + ) + Test.expect(scriptResult, Test.beSucceeded()) + + let aliases = scriptResult.returnValue! as! {String: String} + + Test.assertEqual(nil, aliases[ALIAS]) + Test.assertEqual(TEMPLATE_ID, aliases[NEW_ALIAS]!) +} + +access(all) +fun testShouldDeprecateFlixWithTemplateId() { + Test.loadSnapshot(name: FLIX_PUBLISHED_SNAPSHOT) + + let lookupScriptResultBefore = executeScript( + "../scripts/lookup.cdc", + [REGISTRY_OWNER.address, TEMPLATE_ID, REGISTRY_NAME] + ) + let flixBefore = lookupScriptResultBefore.returnValue! as! FLIXSchema_draft.FLIX + Test.assertEqual("active", flixBefore.status) + + let txResult = executeTransaction( + "../transactions/deprecate_flix.cdc", + [TEMPLATE_ID, REGISTRY_NAME], + REGISTRY_OWNER + ) + Test.expect(txResult, Test.beSucceeded()) + + let typ = Type() + let events = Test.eventsOfType(typ) + let event = events[0] as! FLIXRegistry.Deprecated + Test.assertEqual(1, events.length) + Test.assertEqual(REGISTRY_NAME, event.registryName) + Test.assertEqual(REGISTRY_OWNER.address, event.registryOwner) + Test.assertEqual(TEMPLATE_ID, event.id) + + let lookupScriptResultAfter = executeScript( + "../scripts/lookup.cdc", + [REGISTRY_OWNER.address, TEMPLATE_ID, REGISTRY_NAME] + ) + + Test.expect(lookupScriptResultAfter, Test.beSucceeded()) + + let flixAfter = lookupScriptResultAfter.returnValue! as! FLIXSchema_draft.FLIX + Test.assertEqual("deprecated", flixAfter.status) +} + +access(all) +fun testShouldRemoveFlix() { + Test.loadSnapshot(name: FLIX_PUBLISHED_SNAPSHOT) + + let removeTxResult = executeTransaction( + "../transactions/remove_flix.cdc", + [TEMPLATE_ID, REGISTRY_NAME], + REGISTRY_OWNER + ) + Test.expect(removeTxResult, Test.beSucceeded()) + + let typ = Type() + let events = Test.eventsOfType(typ) + let event = events[0] as! FLIXRegistry.Removed + Test.assertEqual(1, events.length) + Test.assertEqual(REGISTRY_NAME, event.registryName) + Test.assertEqual(REGISTRY_OWNER.address, event.registryOwner) + Test.assertEqual(TEMPLATE_ID, event.id) + + let scriptResult = executeScript( + "../scripts/get_registry_size.cdc", + [REGISTRY_OWNER.address, REGISTRY_NAME] + ) + Test.expect(scriptResult, Test.beSucceeded()) + + let registrySize = scriptResult.returnValue! as! Int + Test.assertEqual(0, registrySize) +} + +access(all) +fun testShouldNotEmitEventWhenRemovingNonexistentFlix() { + Test.loadSnapshot(name: REGISTRY_CREATED_SNAPSHOT) + + let removeTxResult = executeTransaction( + "../transactions/remove_flix.cdc", + [TEMPLATE_ID, REGISTRY_NAME], + REGISTRY_OWNER + ) + Test.expect(removeTxResult, Test.beSucceeded()) + + let typ = Type() + Test.assertEqual(0, Test.eventsOfType(typ).length) + + let scriptResult = executeScript( + "../scripts/get_registry_size.cdc", + [REGISTRY_OWNER.address, REGISTRY_NAME] + ) + Test.expect(scriptResult, Test.beSucceeded()) + + let registrySize = scriptResult.returnValue! as! Int + Test.assertEqual(0, registrySize) +} + +access(all) +fun testShouldThrowExeptionWhenDeprecatingNonexistentFlix() { + Test.loadSnapshot(name: REGISTRY_CREATED_SNAPSHOT) + + Test.expectFailure(fun(): Void { + let txResult = executeTransaction( + "../transactions/deprecate_flix.cdc", + [TEMPLATE_ID, REGISTRY_NAME], + REGISTRY_OWNER + ) + Test.expect(txResult, Test.beFailed()) + let err: Test.Error? = txResult.error + panic(err!.message) // to trick expectFaliure, so we can match on the substring of the actual error + }, errorMessageSubstring: "FLIX does not exist with the given id or alias: aTestId") + +} + +access(all) +fun testShouldCreatePublicPath() { + Test.assertEqual(/public/flix_test, FLIXRegistry.PublicPath(name: "test")) +} + +access(all) +fun testShouldCreateStoragePath() { + Test.assertEqual(/storage/flix_test, FLIXRegistry.StoragePath(name: "test")) +} \ No newline at end of file diff --git a/registry/cadence/test/flix_schema_v1_1_0_test.cdc b/registry/cadence/test/flix_schema_v1_1_0_test.cdc new file mode 100644 index 0000000..69cbab0 --- /dev/null +++ b/registry/cadence/test/flix_schema_v1_1_0_test.cdc @@ -0,0 +1,365 @@ +import Test +import BlockchainHelpers +import "FLIXSchema_v1_1_0" +import "FLIXRegistry" + +access(all) let REGISTRY_OWNER = Test.createAccount() +access(all) let TEMPLATE_ID = "aTestId" +access(all) let ALIAS = "anAlias" +access(all) let NEW_ALIAS = "aNewAlias" +access(all) let REGISTRY_NAME = "someName" +access(all) let CADENCE_BODY_HASH = "someHash" +access(all) let SCHEMA_VERSION = "1.1.0" + +access(all) let CONTRACT_DEPLOYED_SNAPSHOT = "contract deployed" +access(all) let REGISTRY_CREATED_SNAPSHOT = "registry created" +access(all) let FLIX_PUBLISHED_SNAPSHOT = "flix published" + +access(all) +fun setup() { + + let flixRegistryInterfaceErr = Test.deployContract( + name: "FLIXRegistryInterface", + path: "../contracts/flix_registry_interface.cdc", + arguments: [] + ) + Test.expect(flixRegistryInterfaceErr, Test.beNil()) + + let flixRegistryErr = Test.deployContract( + name: "FLIXRegistry", + path: "../contracts/flix_registry.cdc", + arguments: [] + ) + Test.expect(flixRegistryErr, Test.beNil()) + + let flix_schema_v1_1_0 = Test.deployContract( + name: "FLIXSchema_v1_1_0", + path: "../contracts/flix_schema_v1_1_0.cdc", + arguments: [] + ) + Test.expect(flix_schema_v1_1_0, Test.beNil()) + Test.createSnapshot(name: CONTRACT_DEPLOYED_SNAPSHOT) + + let createTxResult = executeTransaction( + "../transactions/create_registry.cdc", + [REGISTRY_NAME], + REGISTRY_OWNER + ) + Test.expect(createTxResult, Test.beSucceeded()) + Test.createSnapshot(name: REGISTRY_CREATED_SNAPSHOT) + + let flixData = FLIXSchema_v1_1_0.Data( + type: "transaction", + interface: "asadf23234...fas234234", + messages: [ + FLIXSchema_v1_1_0.Message( + key: "title", + i18n: [ + FLIXSchema_v1_1_0.I18n( + tag: "en-US", + translation: "Transfer FLOW", + ), + FLIXSchema_v1_1_0.I18n( + tag: "fr-FR", + translation: "FLOW de transfert", + ), + FLIXSchema_v1_1_0.I18n( + tag: "zh-CN", + translation: "转移流程", + ) + ] + ), + FLIXSchema_v1_1_0.Message( + key: "description", + i18n: [ + FLIXSchema_v1_1_0.I18n( + tag: "en-US", + translation: "Transfer {amount} FLOW to {to}", + ), + FLIXSchema_v1_1_0.I18n( + tag: "fr-FR", + translation: "Transférez {amount} FLOW à {to}", + ), + FLIXSchema_v1_1_0.I18n( + tag: "zh-CN", + translation: "将 {amount} FLOW 转移到 {to}", + ) + ] + ), + FLIXSchema_v1_1_0.Message( + key: "signer", + i18n: [ + FLIXSchema_v1_1_0.I18n( + tag: "en-US", + translation: "Sign this message to transfer FLOW", + ), + FLIXSchema_v1_1_0.I18n( + tag: "fr-FR", + translation: "Signez ce message pour transférer FLOW.", + ), + FLIXSchema_v1_1_0.I18n( + tag: "zh-CN", + translation: "签署此消息以转移FLOW。", + ) + ] + ) + ], + cadence: FLIXSchema_v1_1_0.Cadence( + body: "import \"FlowToken\"\ntransaction(amount: UFix64, to: Address) {\n let vault: @FungibleToken.Vault\n prepare(signer: AuthAccount) {\n self.vault <- signer\n .borrow<&{FungibleToken.Provider}>(from: /storage/flowTokenVault)!\n .withdraw(amount: amount)\n\n self.vault <- FungibleToken.getVault(signer)\n }\n execute {\n getAccount(to)\n .getCapability(/public/flowTokenReceiver)!\n .borrow<&{FungibleToken.Receiver}>()!\n .deposit(from: <-self.vault)\n }\n}", + networkPins: [ + FLIXSchema_v1_1_0.NetworkPin( + network: "mainnet", + pinSelf: "186e262ce6fe06b5075ec6569a0e5482a79c471881182612d8e4a665c2977f3e" + ), + FLIXSchema_v1_1_0.NetworkPin( + network: "testnet", + pinSelf: "f93977d7a297f559e97259cb2a95fed0f87cfeec46c5257a26adc26a260d6c4c" + ) + ] + ), + dependencies: [ + FLIXSchema_v1_1_0.Dependency( + contracts: [ + FLIXSchema_v1_1_0.Contract( + contract: "FlowToken", + networks: [ + FLIXSchema_v1_1_0.Network( + network: "mainnet", + address: "0x1654653399040a61", + dependencyPinBlockHeight: 10123123123, + dependencyPin: FLIXSchema_v1_1_0.DependencyPin( + pin: "c8cb7cc7a1c2a329de65d83455016bc3a9b53f9668c74ef555032804bac0b25b", + pinSelf: "38d0cca4b74c4e88213df636b4cfc2eb6e86fd8b2b84579d3b9bffab3e0b1fcb", + pinContractName: "FlowToken", + pinContractAddress: "0x1654653399040a61", + imports: [ + FLIXSchema_v1_1_0.Import( + pin: "b8a3ed26c222ed67016a28021d8fee5603b948533cbc992b3c90f71a61b2b312", + pinSelf: "7bc3056ba5d39d130f45411c2c05bb549db8ce727c11a1cb821136a621be27fb", + pinContractName: "FungibleToken", + pinContractAddress: "0xf233dcee88fe0abe", + imports: [] + ) + ] + ) + ), + FLIXSchema_v1_1_0.Network( + network: "testnet", + address: "0x7e60df042a9c0868", + dependencyPinBlockHeight: 10123123123, + dependencyPin: FLIXSchema_v1_1_0.DependencyPin( + pin: "c8cb7cc7a1c2a329de65d83455016bc3a9b53f9668c74ef555032804bac0b25b", + pinSelf: "38d0cca4b74c4e88213df636b4cfc2eb6e86fd8b2b84579d3b9bffab3e0b1fcb", + pinContractName: "FlowToken", + pinContractAddress: "0x7e60df042a9c0868", + imports: [ + FLIXSchema_v1_1_0.Import( + pin: "b8a3ed26c222ed67016a28021d8fee5603b948533cbc992b3c90f71a61b2b312", + pinSelf: "7bc3056ba5d39d130f45411c2c05bb549db8ce727c11a1cb821136a621be27fb", + pinContractName: "FungibleToken", + pinContractAddress: "0x9a0766d93b6608b7", + imports: [] + ) + ] + ) + ) + ] + ) + ] + ) + ], + parameters: [ + FLIXSchema_v1_1_0.Parameter( + label: "amount", + index: 0, + type: "UFix64", + messages: [ + FLIXSchema_v1_1_0.Message( + key: "title", + i18n: [ + FLIXSchema_v1_1_0.I18n( + tag: "en-US", + translation: "Amount" + ), + FLIXSchema_v1_1_0.I18n( + tag: "fr-FR", + translation: "Montant" + ), + FLIXSchema_v1_1_0.I18n( + tag: "zh-CN", + translation: "数量" + ) + ] + ), + FLIXSchema_v1_1_0.Message( + key: "description", + i18n: [ + FLIXSchema_v1_1_0.I18n( + tag: "en-US", + translation: "Amount of FLOW token to transfer" + ), + FLIXSchema_v1_1_0.I18n( + tag: "fr-FR", + translation: "Quantité de token FLOW à transférer" + ), + FLIXSchema_v1_1_0.I18n( + tag: "zh-CN", + translation: "要转移的 FLOW 代币数量" + ) + ]) + ], + balance: [ + FLIXSchema_v1_1_0.Balance( + network: "mainnet", + pin: "A.0xABC123DEF456.FlowToken" + ), + FLIXSchema_v1_1_0.Balance( + network: "testnet", + pin: "A.0xXYZ678DEF123.FlowToken" + ) + ] + ), + FLIXSchema_v1_1_0.Parameter( + label: "to", + index: 1, + type: "Address", + messages: [ + FLIXSchema_v1_1_0.Message( + key: "title", + i18n: [ + FLIXSchema_v1_1_0.I18n( + tag: "en-US", + translation: "To" + ), + FLIXSchema_v1_1_0.I18n( + tag: "fr-FR", + translation: "Pour" + ), + FLIXSchema_v1_1_0.I18n( + tag: "zh-CN", + translation: "到" + ) + ] + ), + FLIXSchema_v1_1_0.Message( + key: "description", + i18n: [ + FLIXSchema_v1_1_0.I18n( + tag: "en-US", + translation: "Recipient of the FLOW token transfer" + ), + FLIXSchema_v1_1_0.I18n( + tag: "fr-FR", + translation: "Le compte vers lequel transférer les jetons FLOW" + ), + FLIXSchema_v1_1_0.I18n( + tag: "zh-CN", + translation: "将 FLOW 代币转移到的帐户" + ) + ] + ) + ], + balance: [] + ) + ] + ) + + let flix = FLIXSchema_v1_1_0.FLIX( + id: TEMPLATE_ID, + data: flixData, + cadenceBodyHash: CADENCE_BODY_HASH + ) + + let publishTxResult = executeTransaction( + "../transactions/publish_flix_v1_1_0.cdc", + [ALIAS, flix, REGISTRY_NAME], + REGISTRY_OWNER + ) + Test.expect(publishTxResult, Test.beSucceeded()) + Test.createSnapshot(name: FLIX_PUBLISHED_SNAPSHOT) +} + +access(all) +fun testShouldEmitContractInitializedEvent() { + Test.loadSnapshot(name: REGISTRY_CREATED_SNAPSHOT) + + let typ = Type() + Test.assertEqual(1, Test.eventsOfType(typ).length) +} + +access(all) +fun testShouldEmitRegistryCreatedEvent() { + Test.loadSnapshot(name: REGISTRY_CREATED_SNAPSHOT) + + let typ = Type() + let events = Test.eventsOfType(typ) + let event = events[0] as! FLIXRegistry.RegistryCreated + Test.assertEqual(1, events.length) + Test.assertEqual(REGISTRY_NAME, event.name) +} + +access(all) +fun testShouldEmitEventAfterFlixPublished() { + Test.loadSnapshot(name: FLIX_PUBLISHED_SNAPSHOT) + + let typ = Type() + let events = Test.eventsOfType(typ) + let event = events[0] as! FLIXRegistry.Published + Test.assertEqual(1, events.length) + Test.assertEqual(REGISTRY_NAME, event.registryName) + Test.assertEqual(REGISTRY_OWNER.address, event.registryOwner) + Test.assertEqual(ALIAS, event.alias) + Test.assertEqual(TEMPLATE_ID, event.id) + Test.assertEqual(CADENCE_BODY_HASH, event.cadenceBodyHash) +} + +access(all) +fun testShouldContainFlixAfterPublished() { + Test.loadSnapshot(name: FLIX_PUBLISHED_SNAPSHOT) + + let scriptResult = executeScript( + "../scripts/get_registry_size.cdc", + [REGISTRY_OWNER.address, REGISTRY_NAME] + ) + Test.expect(scriptResult, Test.beSucceeded()) + + let registrySize = scriptResult.returnValue! as! Int + Test.assertEqual(1, registrySize) +} + +access(all) +fun testShouldLookupFlixAfterPublished() { + Test.loadSnapshot(name: FLIX_PUBLISHED_SNAPSHOT) + + let lookupScriptResult = executeScript( + "../scripts/lookup.cdc", + [REGISTRY_OWNER.address, ALIAS, REGISTRY_NAME] + ) + + Test.expect(lookupScriptResult, Test.beSucceeded()) + + let flix = lookupScriptResult.returnValue! as! FLIXSchema_v1_1_0.FLIX + Test.assertEqual(TEMPLATE_ID, flix.id) + Test.assertEqual("transaction", flix.getData().type) + Test.assertEqual(CADENCE_BODY_HASH, flix.cadenceBodyHash) + Test.assertEqual("active", flix.status) + Test.assertEqual(SCHEMA_VERSION, flix.getVersion()) +} + +access(all) +fun testShouldResolveFlixAfterPublished() { + Test.loadSnapshot(name: FLIX_PUBLISHED_SNAPSHOT) + + let resolveScriptResult = executeScript( + "../scripts/resolve.cdc", + [REGISTRY_OWNER.address, CADENCE_BODY_HASH, REGISTRY_NAME] + ) + + Test.expect(resolveScriptResult, Test.beSucceeded()) + + let resolvedFlix = resolveScriptResult.returnValue! as! FLIXSchema_v1_1_0.FLIX + Test.assertEqual(TEMPLATE_ID, resolvedFlix.id) + Test.assertEqual("transaction", resolvedFlix.getData().type) + Test.assertEqual(CADENCE_BODY_HASH, resolvedFlix.cadenceBodyHash) + Test.assertEqual("active", resolvedFlix.status) +} diff --git a/registry/cadence/transactions/create_registry.cdc b/registry/cadence/transactions/create_registry.cdc new file mode 100644 index 0000000..f1f6493 --- /dev/null +++ b/registry/cadence/transactions/create_registry.cdc @@ -0,0 +1,17 @@ +import "FLIXRegistry" +import "FLIXRegistryInterface" + +transaction(name: String) { + + prepare(signer: AuthAccount) { + // Check if the account already has a Registry + if signer.borrow<&FLIXRegistry.Registry>(from: FLIXRegistry.StoragePath(name: name)) == nil { + // Create a new Registry resource + let registry <- FLIXRegistry.createRegistry(name: name) + + // Save it with restricted access + signer.save(<-registry, to: FLIXRegistry.StoragePath(name: name)) + signer.link<&FLIXRegistry.Registry{FLIXRegistryInterface.Queryable}>(FLIXRegistry.PublicPath(name: name), target: FLIXRegistry.StoragePath(name: name)) + } + } +} \ No newline at end of file diff --git a/registry/cadence/transactions/deprecate_flix.cdc b/registry/cadence/transactions/deprecate_flix.cdc new file mode 100644 index 0000000..2697e42 --- /dev/null +++ b/registry/cadence/transactions/deprecate_flix.cdc @@ -0,0 +1,11 @@ +import "FLIXRegistry" + +transaction(idOrAlias: String, registryName: String) { + + prepare(signer: AuthAccount) { + let registry = signer.borrow<&FLIXRegistry.Registry>(from: FLIXRegistry.StoragePath(name: registryName)) + ?? panic("Could not borrow a reference to the Registry") + + registry.deprecate(idOrAlias: idOrAlias) + } +} diff --git a/registry/cadence/transactions/link_alias.cdc b/registry/cadence/transactions/link_alias.cdc new file mode 100644 index 0000000..5ecdd97 --- /dev/null +++ b/registry/cadence/transactions/link_alias.cdc @@ -0,0 +1,11 @@ +import "FLIXRegistry" + +transaction(alias: String, templateId: String, registryName: String) { + + prepare(signer: AuthAccount) { + let registry = signer.borrow<&FLIXRegistry.Registry>(from: FLIXRegistry.StoragePath(name: registryName)) + ?? panic("Could not borrow a reference to the Registry") + + registry.link(alias: alias, id: templateId) + } +} \ No newline at end of file diff --git a/registry/cadence/transactions/publish_flix.cdc b/registry/cadence/transactions/publish_flix.cdc new file mode 100644 index 0000000..53a573f --- /dev/null +++ b/registry/cadence/transactions/publish_flix.cdc @@ -0,0 +1,13 @@ +import "FLIXRegistry" +import "FLIXSchema_draft" + +transaction(alias: String, templateId: String, jsonBody: String, cadenceBodyHash: String, registryName: String) { + + prepare(signer: AuthAccount) { + let registry = signer.borrow<&FLIXRegistry.Registry>(from: FLIXRegistry.StoragePath(name: registryName)) + ?? panic("Could not borrow a reference to the Registry") + + let newFlix = FLIXSchema_draft.FLIX(templateId: templateId, jsonBody: jsonBody, cadenceBodyHash: cadenceBodyHash) + registry.publish(alias: alias, flix: newFlix) + } +} \ No newline at end of file diff --git a/registry/cadence/transactions/publish_flix_v1_1_0.cdc b/registry/cadence/transactions/publish_flix_v1_1_0.cdc new file mode 100644 index 0000000..69aa6ec --- /dev/null +++ b/registry/cadence/transactions/publish_flix_v1_1_0.cdc @@ -0,0 +1,12 @@ +import "FLIXRegistry" +import "FLIXSchema_v1_1_0" + +transaction(alias: String, flix: FLIXSchema_v1_1_0.FLIX, registryName: String) { + + prepare(signer: AuthAccount) { + let registry = signer.borrow<&FLIXRegistry.Registry>(from: FLIXRegistry.StoragePath(name: registryName)) + ?? panic("Could not borrow a reference to the Registry") + registry.publish(alias: alias, flix: flix) + } +} + diff --git a/registry/cadence/transactions/remove_flix.cdc b/registry/cadence/transactions/remove_flix.cdc new file mode 100644 index 0000000..13963ed --- /dev/null +++ b/registry/cadence/transactions/remove_flix.cdc @@ -0,0 +1,14 @@ +import "FLIXRegistry" +import "FLIXRegistryInterface" + +transaction(id: String, registryName: String) { + + prepare(signer: AuthAccount) { + // Borrow a reference to the Registry with removal capability + let registry = signer.borrow<&FLIXRegistry.Registry{FLIXRegistryInterface.Removable}>(from: FLIXRegistry.StoragePath(name: registryName)) + ?? panic("Could not borrow a reference to the Registry") + + // Remove the FLIX item using the provided id + registry.remove(id: id) + } +} diff --git a/registry/cadence/transactions/unlink_alias.cdc b/registry/cadence/transactions/unlink_alias.cdc new file mode 100644 index 0000000..b20b7a8 --- /dev/null +++ b/registry/cadence/transactions/unlink_alias.cdc @@ -0,0 +1,11 @@ +import "FLIXRegistry" + +transaction(alias: String, registryName: String) { + + prepare(signer: AuthAccount) { + let registry = signer.borrow<&FLIXRegistry.Registry>(from: FLIXRegistry.StoragePath(name: registryName)) + ?? panic("Could not borrow a reference to the Registry") + + registry.unlink(alias: alias) + } +} \ No newline at end of file diff --git a/registry/flow.json b/registry/flow.json new file mode 100644 index 0000000..f4f4bd8 --- /dev/null +++ b/registry/flow.json @@ -0,0 +1,69 @@ +{ + "contracts": { + "FLIXRegistry": { + "source": "cadence/contracts/flix_registry.cdc", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "testing": "0000000000000007" + } + }, + "FLIXRegistryInterface": { + "source": "cadence/contracts/flix_registry_interface.cdc", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "testing": "0000000000000007" + } + }, + "FLIXSchema_draft": { + "source": "cadence/contracts/flix_schema_draft.cdc", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "testing": "0000000000000007" + } + }, + "FLIXSchema_v1_1_0": { + "source": "cadence/contracts/flix_schema_v1_1_0.cdc", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "testing": "0000000000000007" + } + } + }, + "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": "f8d6e0586b0a20c7", + "key": "6d12eebfef9866c9b6fa92b97c6e705c26a1785b1e7944da701fc545a51d4673" + }, + "emulator-flix": { + "address": "179b6b1cb6755e31", + "key": "573b0db3fe91458e2aceefb8318d6daf3aee2af986a850cbf27a8ffff8a4ef9f" + }, + "testnet-account": { + "address": "0e2b3f46c2f076a0", + "key": { + "type": "file", + "location": "testnet-account.pkey" + } + } + }, + "deployments": { + "emulator": { + "emulator-account": [ + "FLIXRegistry" + ] + }, + "testnet": { + "testnet-account" : [ + "FLIXRegistryInterface", + "FLIXRegistry", + "FLIXSchema_v1_1_0" + ] + } + } +} \ No newline at end of file