diff --git a/packages/wasm-sdk/tests/unit/data-contract.spec.mjs b/packages/wasm-sdk/tests/unit/data-contract.spec.mjs index e04eb4f21c4..9f6df0fa5d5 100644 --- a/packages/wasm-sdk/tests/unit/data-contract.spec.mjs +++ b/packages/wasm-sdk/tests/unit/data-contract.spec.mjs @@ -1,23 +1,287 @@ import init, * as sdk from '../../dist/sdk.compressed.js'; -import contractFixture from './fixtures/data-contract-crypto-card-game.mjs'; +import contractFixtureV0 from './fixtures/data-contract-v0-crypto-card-game.mjs'; +import contractFixtureV1 from './fixtures/data-contract-v1-with-docs-tokens-groups.mjs'; -const PLATFORM_VERSION = 1; +// Platform version constants +const PLATFORM_VERSION_CONTRACT_V0 = 1; +const PLATFORM_VERSION_CONTRACT_V1 = 9; // V1 contracts introduced in Platform v9 + +// Platform version compatibility ranges +const V0_COMPATIBLE_VERSIONS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; // V0 works across all versions +const V1_COMPATIBLE_VERSIONS = [9, 10]; // V1 only works from version 9+ +const V0_ONLY_VERSIONS = [1, 2, 3, 4, 5, 6, 7, 8]; // Versions that only support V0 +const LATEST_KNOWN_VERSION = Math.max(...V0_COMPATIBLE_VERSIONS); + +// Helper function for testing contract compatibility across versions +const testContractAcrossVersions = ( + contractFixture, + contractName, + compatibleVersions, + incompatibleVersions = [], +) => { + compatibleVersions.forEach((version) => { + it(`should work with platform version ${version}`, () => { + const contract = sdk.DataContract.fromJSON(contractFixture, version); + expect(contract).to.be.ok(); + expect(contract.id()).to.equal(contractFixture.id); + + const roundTripped = contract.toJSON(); + expect(roundTripped.id).to.equal(contractFixture.id); + + contract.free(); + }); + }); + + incompatibleVersions.forEach((version) => { + it(`should fail with platform version ${version}`, () => { + expect(() => { + sdk.DataContract.fromJSON(contractFixture, version); + }).to.throw(/unknown version|dpp unknown version/); + }); + }); +}; describe('DataContract', () => { before(async () => { await init(); }); - it('should create a contract from JSON and expose identifiers', async () => { - const contract = sdk.DataContract.fromJSON(contractFixture, PLATFORM_VERSION); + describe('Contract Creation', () => { + it('should create a V0 contract from JSON and expose all properties', async () => { + const contract = sdk.DataContract.fromJSON(contractFixtureV0, PLATFORM_VERSION_CONTRACT_V0); + + expect(contract).to.be.ok(); + expect(contract.id()).to.equal(contractFixtureV0.id); + + const roundTripped = contract.toJSON(); + expect(roundTripped).to.be.an('object'); + expect(roundTripped.id).to.equal(contractFixtureV0.id); + expect(roundTripped.ownerId).to.equal(contractFixtureV0.ownerId); + expect(roundTripped.version).to.equal(contractFixtureV0.version); + expect(roundTripped.$format_version).to.equal(contractFixtureV0.$format_version); + expect(roundTripped.config).to.deep.equal(contractFixtureV0.config); + expect(roundTripped.documentSchemas).to.deep.equal(contractFixtureV0.documentSchemas); + + // Verify document schema structure + expect(roundTripped.documentSchemas.card).to.exist(); + expect(roundTripped.documentSchemas.card.properties.name).to.exist(); + expect(roundTripped.documentSchemas.card.properties.rarity.enum) + .to.deep.equal(['common', 'uncommon', 'rare', 'legendary']); + expect(roundTripped.documentSchemas.card.indices).to.have.length(2); + + contract.free(); + }); + + // TODO: enable test once an SDK fix to support this is merged + it.skip('should create a V1 contract from JSON and expose all properties including tokens and groups', async () => { + const contract = sdk.DataContract.fromJSON(contractFixtureV1, PLATFORM_VERSION_CONTRACT_V1); + + expect(contract).to.be.ok(); + expect(contract.id()).to.equal(contractFixtureV1.id); + + const roundTripped = contract.toJSON(); + expect(roundTripped).to.be.an('object'); + expect(roundTripped.id).to.equal(contractFixtureV1.id); + expect(roundTripped.ownerId).to.equal(contractFixtureV1.ownerId); + expect(roundTripped.version).to.equal(contractFixtureV1.version); + expect(roundTripped.$format_version).to.equal(contractFixtureV1.$format_version); + expect(roundTripped.config.sizedIntegerTypes).to.be.true(); + expect(roundTripped.documentSchemas).to.deep.equal(contractFixtureV1.documentSchemas); + + // Verify V1-specific features + expect(roundTripped.tokens).to.exist(); + expect(roundTripped.tokens['0']).to.exist(); + expect(roundTripped.tokens['0'].baseSupply).to.equal(100); + expect(roundTripped.tokens['0'].conventions.decimals).to.equal(0); + + expect(roundTripped.groups).to.exist(); + expect(roundTripped.groups['0']).to.exist(); + expect(roundTripped.groups['0'].required_power).to.equal(2); + + expect(roundTripped.keywords).to.deep.equal(contractFixtureV1.keywords); + + contract.free(); + }); + + it('should create a contract with only document schemas (no tokens)', () => { + // V0 fixture already has only documents, no tokens - verify it works + const contract = sdk.DataContract.fromJSON(contractFixtureV0, PLATFORM_VERSION_CONTRACT_V0); + const roundTripped = contract.toJSON(); + + expect(roundTripped.documentSchemas.card).to.exist(); + expect(roundTripped.tokens).to.equal(undefined); + + contract.free(); + }); + + it('should create a contract with only tokens (no documents)', () => { + // Use V1 fixture but remove documentSchemas + const contractWithOnlyTokens = { + ...contractFixtureV1, + documentSchemas: {}, + }; + + const contract = sdk.DataContract.fromJSON( + contractWithOnlyTokens, + PLATFORM_VERSION_CONTRACT_V1, + ); + const roundTripped = contract.toJSON(); + + expect(roundTripped.documentSchemas).to.deep.equal({}); + + contract.free(); + }); + }); + + describe('Version Compatibility', () => { + it('should fail to create a V1 contract with V0 platform version', async () => { + expect(() => { + sdk.DataContract.fromJSON(contractFixtureV1, PLATFORM_VERSION_CONTRACT_V0); + }).to.throw(/dpp unknown version.*known versions.*\[0\].*received.*1/); + }); + }); + + describe('Validation', () => { + it('should handle invalid JSON input gracefully', () => { + expect(() => { + sdk.DataContract.fromJSON(null, PLATFORM_VERSION_CONTRACT_V0); + }).to.throw(); + + expect(() => { + sdk.DataContract.fromJSON({}, PLATFORM_VERSION_CONTRACT_V0); + }).to.throw(); + + expect(() => { + sdk.DataContract.fromJSON({ id: 'invalid' }, PLATFORM_VERSION_CONTRACT_V0); + }).to.throw(); + }); + + it('should reject contracts with invalid property values', () => { + // Test invalid Base58 ID + expect(() => { + sdk.DataContract.fromJSON({ + ...contractFixtureV0, + id: 'invalid-not-base58!', + }, PLATFORM_VERSION_CONTRACT_V0); + }).to.throw(); + + // Test negative version number + expect(() => { + sdk.DataContract.fromJSON({ + ...contractFixtureV0, + version: -1, + }, PLATFORM_VERSION_CONTRACT_V0); + }).to.throw(); + + // Test invalid ownerId + expect(() => { + sdk.DataContract.fromJSON({ + ...contractFixtureV0, + ownerId: 'not-a-valid-id', + }, PLATFORM_VERSION_CONTRACT_V0); + }).to.throw(); + }); + + it('should require at least one document type or token', () => { + const contractWithEmptySchemas = { + $format_version: '0', + id: contractFixtureV0.id, + ownerId: contractFixtureV0.ownerId, + version: 1, + config: contractFixtureV0.config, + documentSchemas: {}, + }; + + expect(() => { + sdk.DataContract.fromJSON(contractWithEmptySchemas, PLATFORM_VERSION_CONTRACT_V0); + }).to.throw(/must have at least one document type or token defined/); + }); + }); + + describe('Data Preservation', () => { + it('should preserve all data through JSON round-trip for V0 contract', async () => { + const contract = sdk.DataContract.fromJSON(contractFixtureV0, PLATFORM_VERSION_CONTRACT_V0); + const roundTripped = contract.toJSON(); + + // Create a new contract from the round-tripped JSON + const contract2 = sdk.DataContract.fromJSON(roundTripped, PLATFORM_VERSION_CONTRACT_V0); + const roundTripped2 = contract2.toJSON(); + + expect(roundTripped2).to.deep.equal(roundTripped); + + contract.free(); + contract2.free(); + }); + + it('should preserve all data through JSON round-trip for V1 contract', async () => { + const contract = sdk.DataContract.fromJSON(contractFixtureV1, PLATFORM_VERSION_CONTRACT_V1); + const roundTripped = contract.toJSON(); + + // Create a new contract from the round-tripped JSON + const contract2 = sdk.DataContract.fromJSON(roundTripped, PLATFORM_VERSION_CONTRACT_V1); + const roundTripped2 = contract2.toJSON(); + + expect(roundTripped2).to.deep.equal(roundTripped); + + contract.free(); + contract2.free(); + }); + }); + + describe('Memory Management', () => { + it('should handle memory management properly with multiple contracts', async () => { + const contract1 = sdk.DataContract.fromJSON(contractFixtureV0, PLATFORM_VERSION_CONTRACT_V0); + const contract2 = sdk.DataContract.fromJSON(contractFixtureV1, PLATFORM_VERSION_CONTRACT_V1); + + expect(contract1.id()).to.equal(contractFixtureV0.id); + expect(contract2.id()).to.equal(contractFixtureV1.id); + + contract1.free(); + contract2.free(); + }); + }); + + describe('Platform Version Compatibility Matrix', () => { + describe('V0 Contract Compatibility', () => { + testContractAcrossVersions(contractFixtureV0, 'V0', V0_COMPATIBLE_VERSIONS); + }); + + describe('V1 Contract Compatibility', () => { + testContractAcrossVersions(contractFixtureV1, 'V1', V1_COMPATIBLE_VERSIONS, V0_ONLY_VERSIONS); + }); + + describe('Edge Cases', () => { + it('should fail with invalid version numbers', () => { + const invalidVersions = [ + 0, // Zero version + -1, // Negative version + LATEST_KNOWN_VERSION + 1, // One beyond latest known + LATEST_KNOWN_VERSION * 10, // Far beyond reasonable range + ]; + + invalidVersions.forEach((version) => { + expect(() => { + sdk.DataContract.fromJSON(contractFixtureV0, version); + }).to.throw(/unknown version/); + }); + }); - expect(contract).to.be.ok(); - expect(contract.id()).to.equal(contractFixture.id); + it('should handle version boundary correctly at V9 transition', () => { + // V0 contract should work in V9 (backward compatibility) + const contract = sdk.DataContract.fromJSON(contractFixtureV0, 9); + expect(contract.id()).to.equal(contractFixtureV0.id); + contract.free(); - const roundTripped = contract.toJSON(); - expect(roundTripped).to.be.an('object'); - expect(roundTripped.id).to.equal(contractFixture.id); + // V1 contract should work in V9 (first supported version) + const contractV1 = sdk.DataContract.fromJSON(contractFixtureV1, 9); + expect(contractV1.id()).to.equal(contractFixtureV1.id); + contractV1.free(); - contract.free(); + // V1 contract should fail in V8 (last unsupported version) + expect(() => { + sdk.DataContract.fromJSON(contractFixtureV1, 8); + }).to.throw(/dpp unknown version/); + }); + }); }); }); diff --git a/packages/wasm-sdk/tests/unit/fixtures/data-contract-crypto-card-game.mjs b/packages/wasm-sdk/tests/unit/fixtures/data-contract-v0-crypto-card-game.mjs similarity index 100% rename from packages/wasm-sdk/tests/unit/fixtures/data-contract-crypto-card-game.mjs rename to packages/wasm-sdk/tests/unit/fixtures/data-contract-v0-crypto-card-game.mjs diff --git a/packages/wasm-sdk/tests/unit/fixtures/data-contract-v1-with-docs-tokens-groups.mjs b/packages/wasm-sdk/tests/unit/fixtures/data-contract-v1-with-docs-tokens-groups.mjs new file mode 100644 index 00000000000..11d1ae2f6df --- /dev/null +++ b/packages/wasm-sdk/tests/unit/fixtures/data-contract-v1-with-docs-tokens-groups.mjs @@ -0,0 +1,283 @@ +const contract = { + $format_version: '1', + id: 'Afk9QSj9UDE14K1y9y3iSx6kUSm5LLmhbdAvPvWL4P2i', + config: { + $format_version: '1', + canBeDeleted: false, + readonly: false, + keepsHistory: false, + documentsKeepHistoryContractDefault: false, + documentsMutableContractDefault: true, + documentsCanBeDeletedContractDefault: true, + requiresIdentityEncryptionBoundedKey: null, + requiresIdentityDecryptionBoundedKey: null, + sizedIntegerTypes: true, + }, + version: 1, + ownerId: '7XcruVSsGQVSgTcmPewaE4tXLutnW1F6PXxwMbo8GYQC', + schemaDefs: null, + documentSchemas: { + card: { + type: 'object', + documentsMutable: false, + canBeDeleted: true, + transferable: 1, + tradeMode: 1, + creationRestrictionMode: 1, + properties: { + name: { + type: 'string', + description: 'Name of the card', + minLength: 0, + maxLength: 63, + position: 0, + }, + description: { + type: 'string', + description: 'Description of the card', + minLength: 0, + maxLength: 256, + position: 1, + }, + attack: { + type: 'integer', + description: 'Attack power of the card', + position: 2, + }, + defense: { + type: 'integer', + description: 'Defense level of the card', + position: 3, + }, + }, + indices: [ + { + name: 'owner', + properties: [ + { + $ownerId: 'asc', + }, + ], + }, + { + name: 'attack', + properties: [ + { + attack: 'asc', + }, + ], + }, + { + name: 'defense', + properties: [ + { + defense: 'asc', + }, + ], + }, + ], + required: [ + 'name', + 'attack', + 'defense', + ], + additionalProperties: false, + }, + }, + createdAt: 1756237255149, + updatedAt: null, + createdAtBlockHeight: 174305, + updatedAtBlockHeight: null, + createdAtEpoch: 9690, + updatedAtEpoch: null, + groups: { + 0: { + $format_version: '0', + members: { + '7XcruVSsGQVSgTcmPewaE4tXLutnW1F6PXxwMbo8GYQC': 1, + HJDxtN6FJF3U3T9TMLWCqudfJ5VRkaUrxTsRp36djXAG: 1, + }, + required_power: 2, + }, + }, + tokens: { + 0: { + $format_version: '0', + conventions: { + $format_version: '0', + localizations: { + en: { + $format_version: '0', + shouldCapitalize: true, + singularForm: 'stt-99', + pluralForm: 'stt-99s', + }, + }, + decimals: 0, + }, + conventionsChangeRules: { + V0: { + authorized_to_make_change: 'ContractOwner', + admin_action_takers: 'ContractOwner', + changing_authorized_action_takers_to_no_one_allowed: true, + changing_admin_action_takers_to_no_one_allowed: true, + self_changing_admin_action_takers_allowed: true, + }, + }, + baseSupply: 100, + maxSupply: null, + keepsHistory: { + $format_version: '0', + keepsTransferHistory: true, + keepsFreezingHistory: true, + keepsMintingHistory: true, + keepsBurningHistory: true, + keepsDirectPricingHistory: true, + keepsDirectPurchaseHistory: true, + }, + startAsPaused: false, + allowTransferToFrozenBalance: true, + maxSupplyChangeRules: { + V0: { + authorized_to_make_change: 'ContractOwner', + admin_action_takers: 'ContractOwner', + changing_authorized_action_takers_to_no_one_allowed: true, + changing_admin_action_takers_to_no_one_allowed: true, + self_changing_admin_action_takers_allowed: true, + }, + }, + distributionRules: { + $format_version: '0', + perpetualDistribution: { + $format_version: '0', + distributionType: { + BlockBasedDistribution: { + interval: 100, + function: { + FixedAmount: { + amount: 1, + }, + }, + }, + }, + distributionRecipient: 'ContractOwner', + }, + perpetualDistributionRules: { + V0: { + authorized_to_make_change: 'ContractOwner', + admin_action_takers: 'ContractOwner', + changing_authorized_action_takers_to_no_one_allowed: true, + changing_admin_action_takers_to_no_one_allowed: true, + self_changing_admin_action_takers_allowed: true, + }, + }, + preProgrammedDistribution: null, + newTokensDestinationIdentity: '7XcruVSsGQVSgTcmPewaE4tXLutnW1F6PXxwMbo8GYQC', + newTokensDestinationIdentityRules: { + V0: { + authorized_to_make_change: 'ContractOwner', + admin_action_takers: 'ContractOwner', + changing_authorized_action_takers_to_no_one_allowed: true, + changing_admin_action_takers_to_no_one_allowed: true, + self_changing_admin_action_takers_allowed: true, + }, + }, + mintingAllowChoosingDestination: false, + mintingAllowChoosingDestinationRules: { + V0: { + authorized_to_make_change: 'ContractOwner', + admin_action_takers: 'ContractOwner', + changing_authorized_action_takers_to_no_one_allowed: true, + changing_admin_action_takers_to_no_one_allowed: true, + self_changing_admin_action_takers_allowed: true, + }, + }, + changeDirectPurchasePricingRules: { + V0: { + authorized_to_make_change: 'ContractOwner', + admin_action_takers: 'ContractOwner', + changing_authorized_action_takers_to_no_one_allowed: true, + changing_admin_action_takers_to_no_one_allowed: true, + self_changing_admin_action_takers_allowed: true, + }, + }, + }, + marketplaceRules: { + $format_version: '0', + tradeMode: 'NotTradeable', + tradeModeChangeRules: { + V0: { + authorized_to_make_change: 'ContractOwner', + admin_action_takers: 'ContractOwner', + changing_authorized_action_takers_to_no_one_allowed: true, + changing_admin_action_takers_to_no_one_allowed: true, + self_changing_admin_action_takers_allowed: true, + }, + }, + }, + manualMintingRules: { + V0: { + authorized_to_make_change: 'ContractOwner', + admin_action_takers: 'ContractOwner', + changing_authorized_action_takers_to_no_one_allowed: true, + changing_admin_action_takers_to_no_one_allowed: true, + self_changing_admin_action_takers_allowed: true, + }, + }, + manualBurningRules: { + V0: { + authorized_to_make_change: 'ContractOwner', + admin_action_takers: 'ContractOwner', + changing_authorized_action_takers_to_no_one_allowed: true, + changing_admin_action_takers_to_no_one_allowed: true, + self_changing_admin_action_takers_allowed: true, + }, + }, + freezeRules: { + V0: { + authorized_to_make_change: 'ContractOwner', + admin_action_takers: 'ContractOwner', + changing_authorized_action_takers_to_no_one_allowed: true, + changing_admin_action_takers_to_no_one_allowed: true, + self_changing_admin_action_takers_allowed: true, + }, + }, + unfreezeRules: { + V0: { + authorized_to_make_change: 'ContractOwner', + admin_action_takers: 'ContractOwner', + changing_authorized_action_takers_to_no_one_allowed: true, + changing_admin_action_takers_to_no_one_allowed: true, + self_changing_admin_action_takers_allowed: true, + }, + }, + destroyFrozenFundsRules: { + V0: { + authorized_to_make_change: 'ContractOwner', + admin_action_takers: 'ContractOwner', + changing_authorized_action_takers_to_no_one_allowed: true, + changing_admin_action_takers_to_no_one_allowed: true, + self_changing_admin_action_takers_allowed: true, + }, + }, + emergencyActionRules: { + V0: { + authorized_to_make_change: 'ContractOwner', + admin_action_takers: 'ContractOwner', + changing_authorized_action_takers_to_no_one_allowed: true, + changing_admin_action_takers_to_no_one_allowed: true, + self_changing_admin_action_takers_allowed: true, + }, + }, + mainControlGroup: 0, + mainControlGroupCanBeModified: 'ContractOwner', + description: null, + }, + }, + keywords: [ + 'stt-99', + ], + description: null, +}; + +export default contract;