-
Notifications
You must be signed in to change notification settings - Fork 60
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(core): adds CIP20 TxMetadata helper functions
- `toCIP20Metadata` to convert messages to transaction metadata, supporting both an array of strings or single string that will be chunked if > 64 bytes. - `validateMessage` to ensure the correct type and size compliance, compatible with JS runtime validation - `CIP_20_METADATUM_LABEL` is exported for reference
- Loading branch information
Showing
3 changed files
with
147 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import { CustomError } from 'ts-custom-error'; | ||
import { StringUtils } from '@cardano-sdk/util'; | ||
import { TxMetadata } from '../Cardano'; | ||
|
||
export const CIP_20_METADATUM_LABEL = 674n; | ||
const MAX_BYTES = 64; | ||
|
||
export enum MessageValidationFailure { | ||
wrongType = 'WrongType', | ||
oversize = 'Oversize' | ||
} | ||
|
||
export type MessageValidationResult = { | ||
valid: boolean; | ||
failure?: MessageValidationFailure; | ||
}; | ||
|
||
export type CIP20TxMetadataMessage = string; | ||
|
||
export type CIP20TxMetadataArgs = { | ||
// An array of message strings, limited to 64 bytes each | ||
messages: CIP20TxMetadataMessage[]; | ||
}; | ||
|
||
export type ValidationResultMap = Map<CIP20TxMetadataMessage, MessageValidationResult>; | ||
|
||
export class MessageValidationError extends CustomError { | ||
public constructor(failures: ValidationResultMap) { | ||
const m = [failures.entries()].map( | ||
([[message, result]]) => `${result.failure}: ${message.slice(0, Math.max(0, MAX_BYTES + 1))}` | ||
); | ||
super(`The provided message array contains validation errors | ${m}`); | ||
} | ||
} | ||
|
||
/** | ||
* Validate each message for correct type, in the case of JavaScript, and size constraint | ||
* | ||
* @param entry unknown | ||
* @returns Validation result | ||
*/ | ||
export const validateMessage = (entry: unknown): MessageValidationResult => { | ||
if (typeof entry !== 'string') return { failure: MessageValidationFailure.wrongType, valid: false }; | ||
if (StringUtils.byteSize(entry) > MAX_BYTES) return { failure: MessageValidationFailure.oversize, valid: false }; | ||
return { valid: true }; | ||
}; | ||
|
||
/** | ||
* Converts an object containing an array of individual messages into https://cips.cardano.org/cip/CIP-20 compliant | ||
* transaction metadata | ||
* | ||
* @param args CIP20TxMetadataArgs or a string to be transformed into an array | ||
* @returns CIP20-compliant transaction metadata | ||
* @throws Message validation error containing details. Use validateMessage to independently check each message before calling this function | ||
*/ | ||
export const toCIP20Metadata = (args: CIP20TxMetadataArgs | string): TxMetadata => { | ||
const messages = typeof args === 'string' ? StringUtils.chunkByBytes(args, MAX_BYTES) : args.messages; | ||
const invalidMessages: ValidationResultMap = new Map(); | ||
for (const message of messages) { | ||
const result = validateMessage(message); | ||
if (!result.valid) invalidMessages.set(message, result); | ||
} | ||
if (invalidMessages.size > 0) throw new MessageValidationError(invalidMessages); | ||
return new Map([[CIP_20_METADATUM_LABEL, new Map([['msg', messages]])]]); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './cip20'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import { | ||
CIP_20_METADATUM_LABEL, | ||
MessageValidationError, | ||
MessageValidationFailure, | ||
toCIP20Metadata, | ||
validateMessage | ||
} from '../../src/TxMetadata'; | ||
import { Cardano } from '../../src'; | ||
import { TxMetadata } from '../../src/Cardano'; | ||
|
||
describe('TxMetadata.cip20', () => { | ||
const compliantShortMessage = 'Lorem ipsum dolor'; | ||
const compliantMaxMessage = 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean'; | ||
const oversizeMessage = `${compliantMaxMessage}1`; | ||
describe('validateMessage', () => { | ||
it('validates a CIP-20 message if a string and less than or equal to 64 bytes', () => { | ||
expect(validateMessage(compliantShortMessage)).toStrictEqual({ valid: true }); | ||
expect(validateMessage(compliantMaxMessage)).toStrictEqual({ valid: true }); | ||
}); | ||
it('invalidates a CIP-20 message if a string but over 64 bytes', () => { | ||
expect(validateMessage(oversizeMessage)).toStrictEqual({ | ||
failure: MessageValidationFailure.oversize, | ||
valid: false | ||
}); | ||
}); | ||
it('invalidates a CIP-20 message if wrong type', () => { | ||
expect(validateMessage(1 as unknown as string)).toStrictEqual({ | ||
failure: MessageValidationFailure.wrongType, | ||
valid: false | ||
}); | ||
expect(validateMessage({ message: compliantShortMessage } as unknown as string)).toStrictEqual({ | ||
failure: MessageValidationFailure.wrongType, | ||
valid: false | ||
}); | ||
expect(validateMessage([compliantShortMessage] as unknown as string)).toStrictEqual({ | ||
failure: MessageValidationFailure.wrongType, | ||
valid: false | ||
}); | ||
expect( | ||
validateMessage(new Map([[CIP_20_METADATUM_LABEL, compliantShortMessage]]) as unknown as string) | ||
).toStrictEqual({ failure: MessageValidationFailure.wrongType, valid: false }); | ||
}); | ||
}); | ||
describe('toCIP20Metadata', () => { | ||
describe('args object', () => { | ||
it('produces a CIP-20-compliant TxMetadata map', () => { | ||
const metadata = toCIP20Metadata({ messages: [compliantShortMessage] }) as TxMetadata; | ||
expect(metadata.has(CIP_20_METADATUM_LABEL)).toBe(true); | ||
const cip20Metadata = metadata.get(CIP_20_METADATUM_LABEL) as Cardano.MetadatumMap; | ||
expect(cip20Metadata.get('msg')).toStrictEqual([compliantShortMessage]); | ||
}); | ||
it('throws an error if any messages are invalid', () => { | ||
expect(() => | ||
toCIP20Metadata({ | ||
messages: [compliantShortMessage, compliantMaxMessage, oversizeMessage] | ||
}) | ||
).toThrowError(MessageValidationError); | ||
}); | ||
}); | ||
describe('producing a CIP-20-compliant TxMetadata map with a string arg', () => { | ||
test('larger than 64 bytes', () => { | ||
const metadata = toCIP20Metadata(oversizeMessage) as TxMetadata; | ||
expect(metadata.has(CIP_20_METADATUM_LABEL)).toBe(true); | ||
const cip20Metadata = metadata.get(CIP_20_METADATUM_LABEL) as Cardano.MetadatumMap; | ||
expect((cip20Metadata.get('msg') as string[]).length).toBe(2); | ||
}); | ||
test('equal to 64 bytes', () => { | ||
const metadata = toCIP20Metadata(compliantMaxMessage) as TxMetadata; | ||
expect(metadata.has(CIP_20_METADATUM_LABEL)).toBe(true); | ||
const cip20Metadata = metadata.get(CIP_20_METADATUM_LABEL) as Cardano.MetadatumMap; | ||
expect((cip20Metadata.get('msg') as string[]).length).toBe(1); | ||
}); | ||
test('smaller than to 64 bytes', () => { | ||
const metadata = toCIP20Metadata(compliantShortMessage) as TxMetadata; | ||
expect(metadata.has(CIP_20_METADATUM_LABEL)).toBe(true); | ||
const cip20Metadata = metadata.get(CIP_20_METADATUM_LABEL) as Cardano.MetadatumMap; | ||
expect((cip20Metadata.get('msg') as string[]).length).toBe(1); | ||
}); | ||
}); | ||
}); | ||
}); |