diff --git a/packages/standards/src/src44/DescriptorData.ts b/packages/standards/src/src44/DescriptorData.ts index 7d0f2580..eaec65fd 100644 --- a/packages/standards/src/src44/DescriptorData.ts +++ b/packages/standards/src/src44/DescriptorData.ts @@ -17,7 +17,7 @@ import { parseIpfsMedia } from './parseIpfsMedia'; */ export class DescriptorData { - private constructor(private data: SRC44Descriptor) { + private constructor(private data: SRC44Descriptor, private strict: boolean) { this.validate(); } @@ -82,23 +82,28 @@ export class DescriptorData { } /** - * Creates a bare minimum SRC44 descriptor instance. + * Creates a bare minimum, but strict, SRC44 descriptor instance. * @param name The name */ public static create(name?: string) { return new DescriptorData({ vs: 1, nm: name - }); + }, true); } /** * Creates/Parses a SRC44 compliant descriptor string * @param jsonString The SRC44 compliant string. See also [[stringify]] + * @param strict If true, the standard check is more strictly */ - public static parse(jsonString: string) { + public static parse(jsonString: string, strict = true) { try { - return new DescriptorData(JSON.parse(jsonString)); + const json = JSON.parse(jsonString); + if (!strict && !json.vs) { + json.vs = 1; + } + return new DescriptorData(json, strict); // @ts-ignore } catch (e: any) { throw new SRC44ParseException(e.message); @@ -141,7 +146,7 @@ export class DescriptorData { * @throws in case of invalid data. */ public validate() { - validateSRC44(this.raw); + validateSRC44(this.raw, this.strict); } /** diff --git a/packages/standards/src/src44/__tests__/descriptorData.spec.ts b/packages/standards/src/src44/__tests__/descriptorData.spec.ts index 8f547f93..2c68d283 100644 --- a/packages/standards/src/src44/__tests__/descriptorData.spec.ts +++ b/packages/standards/src/src44/__tests__/descriptorData.spec.ts @@ -1,6 +1,19 @@ import {DescriptorData} from '../DescriptorData'; -const TestObject1 = { +const TestObjectNotStrict = { + 'tp': 'foo', + 'nm': 'Bittrex', + 'ds': 'World class exchange at your service', + 'av': {'QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR': 'image/gif'}, + 'bg': {'QmUFc4dyX7TJn5dPxp8CrcDeedoV18owTBUWApYMuF6Koc': 'image/jpeg'}, + 'hp': 'https://bittrex.com', + 'sr': '^[0-9a-fA-F]{24}$', + 'al': 'somealias', + 'xt': 'QmUFc4dyX7TJn5dPxp8CrcDeedoV18owTBUWApYMuF6Koc', + 'sc': ['https://twitter.com/bittrex', 'https://twitter.com/bittrex2'] +}; + +const TestObjectStrict = { 'vs': 1, 'tp': 'cex', 'nm': 'Bittrex', @@ -18,7 +31,7 @@ const TestObject1 = { describe('descriptorData', () => { describe('get', () => { it('should return a human friendly object', () => { - const descriptor = DescriptorData.parse(JSON.stringify(TestObject1)); + const descriptor = DescriptorData.parse(JSON.stringify(TestObjectStrict)); expect(descriptor.get()).toEqual( { 'alias': 'somealias', @@ -47,11 +60,11 @@ describe('descriptorData', () => { }); describe('stringify', () => { it('should stringify as expected', () => { - const descriptor = DescriptorData.parse(JSON.stringify(TestObject1)); + const descriptor = DescriptorData.parse(JSON.stringify(TestObjectStrict)); expect(descriptor.stringify()).toEqual('{\"vs\":1,\"tp\":\"cex\",\"nm\":\"Bittrex\",\"ds\":\"World class exchange at your service\",\"av\":{\"QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR\":\"image/gif\"},\"bg\":{\"QmUFc4dyX7TJn5dPxp8CrcDeedoV18owTBUWApYMuF6Koc\":\"image/jpeg\"},\"hp\":\"https://bittrex.com\",\"sr\":\"^[0-9a-fA-F]{24}$\",\"al\":\"somealias\",\"xt\":\"QmUFc4dyX7TJn5dPxp8CrcDeedoV18owTBUWApYMuF6Koc\",\"sc\":[\"https://twitter.com/bittrex\",\"https://twitter.com/bittrex2\"]}'); }); it('should parse as expected with custom properties', () => { - const t = {...TestObject1, tw: 'Twitter account', xCustom: 1111}; + const t = {...TestObjectStrict, tw: 'Twitter account', xCustom: 1111}; const descriptor = DescriptorData.parse(JSON.stringify(t)); expect(descriptor.version).toBe(1); expect(descriptor.type).toBe('cex'); @@ -76,7 +89,7 @@ describe('descriptorData', () => { it('should throw exception if string is too long', () => { const t = { - ...TestObject1, + ...TestObjectStrict, ds: 'x'.repeat(500) }; expect(() => { @@ -86,7 +99,7 @@ describe('descriptorData', () => { it('should throw exception if object is too large', () => { const t = { - ...TestObject1, + ...TestObjectStrict, ds: 'x'.repeat(384), xc: 'foo'.repeat(500) }; @@ -112,7 +125,7 @@ describe('descriptorData', () => { }); describe('parse', () => { it('should parse as expected', () => { - const descriptor = DescriptorData.parse(JSON.stringify(TestObject1)); + const descriptor = DescriptorData.parse(JSON.stringify(TestObjectStrict)); expect(descriptor.version).toBe(1); expect(descriptor.type).toBe('cex'); expect(descriptor.name).toBe('Bittrex'); @@ -131,8 +144,28 @@ describe('descriptorData', () => { expect(descriptor.extension).toBe('QmUFc4dyX7TJn5dPxp8CrcDeedoV18owTBUWApYMuF6Koc'); expect(descriptor.socialMediaLinks).toEqual(['https://twitter.com/bittrex', 'https://twitter.com/bittrex2']); }); + it('should parse as expected - less strict', () => { + const descriptor = DescriptorData.parse(JSON.stringify(TestObjectNotStrict), false); + expect(descriptor.version).toBe(1); + expect(descriptor.type).toBe('foo'); + expect(descriptor.name).toBe('Bittrex'); + expect(descriptor.avatar).toEqual({ + ipfsCid: 'QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR', + mimeType: 'image/gif' + }); + expect(descriptor.background).toEqual({ + ipfsCid: 'QmUFc4dyX7TJn5dPxp8CrcDeedoV18owTBUWApYMuF6Koc', + mimeType: 'image/jpeg' + }); + expect(descriptor.description).toBe('World class exchange at your service'); + expect(descriptor.alias).toBe('somealias'); + expect(descriptor.homePage).toBe('https://bittrex.com'); + expect(descriptor.sendRule).toEqual(new RegExp('^[0-9a-fA-F]{24}$')); + expect(descriptor.extension).toBe('QmUFc4dyX7TJn5dPxp8CrcDeedoV18owTBUWApYMuF6Koc'); + expect(descriptor.socialMediaLinks).toEqual(['https://twitter.com/bittrex', 'https://twitter.com/bittrex2']); + }); it('should parse as expected with custom properties', () => { - const t = {...TestObject1, tw: 'Twitter account', xCustom: 1111}; + const t = {...TestObjectStrict, tw: 'Twitter account', xCustom: 1111}; const descriptor = DescriptorData.parse(JSON.stringify(t)); expect(descriptor.version).toBe(1); expect(descriptor.type).toBe('cex'); @@ -157,7 +190,7 @@ describe('descriptorData', () => { it('should throw exception if string is too long', () => { const t = { - ...TestObject1, + ...TestObjectStrict, ds: 'x'.repeat(500) }; expect(() => { @@ -167,7 +200,7 @@ describe('descriptorData', () => { it('should throw exception if object is too large', () => { const t = { - ...TestObject1, + ...TestObjectStrict, ds: 'x'.repeat(384), xc: 'foo'.repeat(500) }; @@ -203,41 +236,41 @@ describe('descriptorData', () => { describe('estimateFeePlanck', () => { it('should calculate correct fee', () => { expect(DescriptorData.create('Some name').estimateFeePlanck()).toEqual('20000000'); - expect(DescriptorData.parse(JSON.stringify(TestObject1)).estimateFeePlanck()).toEqual('60000000'); + expect(DescriptorData.parse(JSON.stringify(TestObjectStrict)).estimateFeePlanck()).toEqual('60000000'); expect(DescriptorData.parse(JSON.stringify({ - ...TestObject1, + ...TestObjectStrict, 'custom': 'custom'.repeat(50) })).estimateFeePlanck()).toEqual('80000000'); expect(DescriptorData.parse(JSON.stringify({ - ...TestObject1, + ...TestObjectStrict, 'custom': 'custom'.repeat(75) })).estimateFeePlanck()).toEqual('100000000'); expect(DescriptorData.parse(JSON.stringify({ - ...TestObject1, + ...TestObjectStrict, 'custom': 'custom'.repeat(90) })).estimateFeePlanck()).toEqual('120000000'); }); it('should calculate correct fee - baseFee = 0.02', () => { const BaseFee = 2000000; expect(DescriptorData.create('Some name').estimateFeePlanck(BaseFee)).toEqual('2000000'); - expect(DescriptorData.parse(JSON.stringify(TestObject1)).estimateFeePlanck(BaseFee)).toEqual('6000000'); + expect(DescriptorData.parse(JSON.stringify(TestObjectStrict)).estimateFeePlanck(BaseFee)).toEqual('6000000'); expect(DescriptorData.parse(JSON.stringify({ - ...TestObject1, + ...TestObjectStrict, 'custom': 'custom'.repeat(50) })).estimateFeePlanck(BaseFee)).toEqual('8000000'); expect(DescriptorData.parse(JSON.stringify({ - ...TestObject1, + ...TestObjectStrict, 'custom': 'custom'.repeat(75) })).estimateFeePlanck(BaseFee)).toEqual('10000000'); expect(DescriptorData.parse(JSON.stringify({ - ...TestObject1, + ...TestObjectStrict, 'custom': 'custom'.repeat(90) })).estimateFeePlanck(BaseFee)).toEqual('12000000'); }); it('should throw if 1000 bytes is exceeded', () => { expect(() => { DescriptorData.parse(JSON.stringify({ - ...TestObject1, + ...TestObjectStrict, 'custom': 'some custom content'.repeat(100) })).estimateFeePlanck(); }).toThrow('[SRC44 Validation Error]: Maximum length of 1000 bytes allowed'); diff --git a/packages/standards/src/src44/__tests__/validateSRC44.spec.ts b/packages/standards/src/src44/__tests__/validateSRC44.spec.ts index 68f946e7..16e846e4 100644 --- a/packages/standards/src/src44/__tests__/validateSRC44.spec.ts +++ b/packages/standards/src/src44/__tests__/validateSRC44.spec.ts @@ -20,6 +20,28 @@ describe('validateSRC44', () => { 'sc': ['https://twitter.com/bittrex'] }); }); + + it('should be fine - less strict', () => { + + // no vs and different type + validateSRC44({ + // @ts-ignore + 'tp': 'foo', + 'id': 'id', + 'ac': '895212263565386113', + 'nm': 'Bittrex', + 'ds': 'World class exchange at your service', + 'av': { 'QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR': 'image/gif' }, + 'bg': { 'QmUFc4dyX7TJn5dPxp8CrcDeedoV18owTBUWApYMuF6Koc': 'image/jpeg' }, + 'hp': 'https://bittrex.com', + 'sr': '^[0-9a-fA-F]{24}$', + 'al': 'somealias', + 'xt': 'QmUFc4dyX7TJn5dPxp8CrcDeedoV18owTBUWApYMuF6Koc', + 'sc': ['https://twitter.com/bittrex'], + 'x-custom': 'bla blubb' + }, false); + }); + it('throws error for too large object', () => { expect(() => { validateSRC44({ @@ -154,7 +176,7 @@ describe('validateSRC44', () => { // @ts-ignore tp: 'foo' }); - }).toThrow('tp must be one of [hum,smc,biz,cex,dex,oth] - Got foo'); + }).toThrow('tp must be one of [hum,smc,biz,cex,dex,oth,tok,bot] - Got foo'); }); }); @@ -202,7 +224,7 @@ describe('validateSRC44', () => { nm: 'name', ac: '12432452' }); - }).toThrow('ac must match /^\\d{18,22}$/ - Got 12432452'); + }).toThrow('ac must match /^\\d{10,22}$/ - Got 12432452'); }); it('throws error for beign too large', () => { expect(() => { @@ -211,7 +233,7 @@ describe('validateSRC44', () => { nm: 'name', ac: '74'.repeat(30) }); - }).toThrow('ac must match /^\\d{18,22}$/'); + }).toThrow('ac must match /^\\d{10,22}$/'); }); }); diff --git a/packages/standards/src/src44/parseIpfsMedia.ts b/packages/standards/src/src44/parseIpfsMedia.ts index 93f90ded..f4230d2f 100644 --- a/packages/standards/src/src44/parseIpfsMedia.ts +++ b/packages/standards/src/src44/parseIpfsMedia.ts @@ -1,7 +1,8 @@ /** * Copyright (c) 2022 Signum Network */ -import { SRC44ParseException } from "./exceptions"; +import { SRC44ParseException } from './exceptions'; +import {IpfsMediaType} from './typings'; /** * @@ -11,7 +12,7 @@ import { SRC44ParseException } from "./exceptions"; * @param o * @module standards.SRC44 */ -export function parseIpfsMedia(o: object) { +export function parseIpfsMedia(o: object): IpfsMediaType { if (!o) { return undefined; } diff --git a/packages/standards/src/src44/typings/Descriptor.ts b/packages/standards/src/src44/typings/Descriptor.ts index 14cdd5c8..a91d70a6 100644 --- a/packages/standards/src/src44/typings/Descriptor.ts +++ b/packages/standards/src/src44/typings/Descriptor.ts @@ -1,11 +1,8 @@ /** * Copyright (c) 2022 Signum Network */ -import { SRC44DescriptorType } from './SRC44DescriptorType'; - -interface MediaType { - [key: string]: string; -} +import {SRC44DescriptorType} from './SRC44DescriptorType'; +import {IpfsMediaType} from './IpfsMediaType'; /** * Human friendly descriptor structure @@ -34,11 +31,11 @@ export interface Descriptor { /** * IPFS Media Link for the Avatar */ - avatar?: MediaType; + avatar?: IpfsMediaType; /** * IPFS Media Link for the background image */ - background?: MediaType; + background?: IpfsMediaType; /** * Homepage - maximal 128 characters */ diff --git a/packages/standards/src/src44/typings/IpfsMediaType.ts b/packages/standards/src/src44/typings/IpfsMediaType.ts new file mode 100644 index 00000000..063d3a28 --- /dev/null +++ b/packages/standards/src/src44/typings/IpfsMediaType.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2022 Signum Network + */ + +/** + * Type for media data used in SRC44 + * + * @internal + * @module standards.SRC44 + */ +export interface IpfsMediaType { + /** + * IPFS CID + */ + ipfsCid: string; + /** + * Mime Type, e.g. image/png + */ + mimeType: string; +} diff --git a/packages/standards/src/src44/typings/index.ts b/packages/standards/src/src44/typings/index.ts index 83944960..d49ee7e1 100644 --- a/packages/standards/src/src44/typings/index.ts +++ b/packages/standards/src/src44/typings/index.ts @@ -1,3 +1,4 @@ export * from './Descriptor'; export * from './SRC44Descriptor'; export * from './SRC44DescriptorType'; +export * from './IpfsMediaType'; diff --git a/packages/standards/src/src44/validateSRC44.ts b/packages/standards/src/src44/validateSRC44.ts index 8a1e2a5f..518d8e9c 100644 --- a/packages/standards/src/src44/validateSRC44.ts +++ b/packages/standards/src/src44/validateSRC44.ts @@ -15,9 +15,10 @@ import {parseIpfsMedia} from './parseIpfsMedia'; * * @internal * @param json + * @param strict * @module standards.SRC44 */ -export function validateSRC44(json: SRC44Descriptor) { +export function validateSRC44(json: SRC44Descriptor, strict = true) { const MaxLength = 1000; const DsLength = 384; const NmLength = 24; @@ -25,9 +26,9 @@ export function validateSRC44(json: SRC44Descriptor) { const HpLength = 128; const ScItemLength = 3; const ScItemUrlLength = 92; - const AllowedTypes = ['hum', 'smc', 'biz', 'cex', 'dex', 'oth']; + const AllowedTypes = ['hum', 'smc', 'biz', 'cex', 'dex', 'oth', 'tok', 'bot']; try { - if (json.vs !== 1) { + if (strict && json.vs !== 1) { throw new Error(`vs is required and must be 1 - Got ${json.vs}`); } @@ -51,15 +52,15 @@ export function validateSRC44(json: SRC44Descriptor) { throw new Error(`al must match /^\\w{1,100}$/ - Got ${json.al}`); } - if (json.ac && !/^\d{18,22}$/.test(json.ac)) { - throw new Error(`ac must match /^\\d{18,22}$/ - Got ${json.ac}`); + if (json.ac && !/^\d{10,22}$/.test(json.ac)) { + throw new Error(`ac must match /^\\d{10,22}$/ - Got ${json.ac}`); } // xt is just a IPFS CID string // sr is just a regex string - if (json.tp && AllowedTypes.indexOf(json.tp) < 0) { + if (strict && json.tp && AllowedTypes.indexOf(json.tp) < 0) { throw new Error(`tp must be one of [${AllowedTypes.join(',')}] - Got ${json.tp}`); }