Skip to content

Commit

Permalink
feat(core): adds CIP20 TxMetadata helper functions
Browse files Browse the repository at this point in the history
- `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
rhyslbw committed Aug 21, 2024
1 parent 82669e7 commit 8bc200a
Show file tree
Hide file tree
Showing 3 changed files with 147 additions and 0 deletions.
65 changes: 65 additions & 0 deletions packages/core/src/TxMetadata/cip20.ts
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]])]]);
};
1 change: 1 addition & 0 deletions packages/core/src/TxMetadata/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './cip20';
81 changes: 81 additions & 0 deletions packages/core/test/TxMetadata/cip20.test.ts
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);
});
});
});
});

0 comments on commit 8bc200a

Please sign in to comment.