diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index a98747268..dbfcaea41 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -48,6 +48,7 @@ jobs: path: | ./node_modules ./docs/node_modules + ./packages/ccd-js-gen/node_modules key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} restore-keys: | ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} @@ -104,6 +105,7 @@ jobs: path: | ./node_modules ./docs/node_modules + ./packages/ccd-js-gen/node_modules key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} - name: Generate typedoc documentation diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index 043ad38d9..eaf50592b 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -39,6 +39,7 @@ jobs: path: | ./node_modules ./docs/node_modules + ./packages/ccd-js-gen/node_modules key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}-${{ env.DUMMY }} restore-keys: | ${{ runner.os }}-yarn @@ -83,6 +84,7 @@ jobs: path: | ./node_modules ./docs/node_modules + ./packages/ccd-js-gen/node_modules key: ${{ runner.os }}-yarn - name: Install rust @@ -126,6 +128,7 @@ jobs: path: | ./node_modules ./docs/node_modules + ./packages/ccd-js-gen/node_modules key: ${{ runner.os }}-yarn - name: Get build-debug @@ -162,6 +165,7 @@ jobs: path: | ./node_modules ./docs/node_modules + ./packages/ccd-js-gen/node_modules key: ${{ runner.os }}-yarn - name: Get build-debug @@ -189,6 +193,7 @@ jobs: path: | ./node_modules ./docs/node_modules + ./packages/ccd-js-gen/node_modules key: ${{ runner.os }}-yarn - name: Get build-debug @@ -217,6 +222,7 @@ jobs: path: | ./node_modules ./docs/node_modules + ./packages/ccd-js-gen/node_modules key: ${{ runner.os }}-yarn - name: Get build-debug @@ -245,6 +251,7 @@ jobs: path: | ./node_modules ./docs/node_modules + ./packages/ccd-js-gen/node_modules key: ${{ runner.os }}-yarn - name: Lint markdown @@ -268,6 +275,7 @@ jobs: path: | ./node_modules ./docs/node_modules + ./packages/ccd-js-gen/node_modules key: ${{ runner.os }}-yarn - name: Lint markdown diff --git a/packages/ccd-js-gen/jest.config.ts b/packages/ccd-js-gen/jest.config.ts index f9cd4484d..8264af593 100644 --- a/packages/ccd-js-gen/jest.config.ts +++ b/packages/ccd-js-gen/jest.config.ts @@ -1,14 +1,14 @@ import type { Config } from 'jest'; import type {} from 'ts-jest'; +export const esModules = ['@noble/ed25519', '@concordium/web-sdk']; + const config: Config = { preset: 'ts-jest/presets/js-with-ts-esm', moduleNameMapper: { '^(\\.\\.?\\/.+)\\.js$': '$1', // Remap esmodule file extensions }, - transformIgnorePatterns: [ - 'node_modules/(?!@noble/ed25519)', // @noble/ed25519 is an ES module only - ], + transformIgnorePatterns: [`node_modules/(?!${esModules.join('|')})`], }; export default config; diff --git a/packages/sdk/CHANGELOG.md b/packages/sdk/CHANGELOG.md index 1fb7d1363..8997cbe79 100644 --- a/packages/sdk/CHANGELOG.md +++ b/packages/sdk/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 7.1.0 + +### Added + +- `jsonUnwrapStringify` function, which can be used to unwrap concordium domain types to their inner values before serializing, to ease compatibility with dependants deserializing stringified JSON. + + ## 7.0.3 ### Fixed diff --git a/packages/sdk/package.json b/packages/sdk/package.json index c0e447174..d258c876b 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@concordium/web-sdk", - "version": "7.0.3", + "version": "7.1.0", "license": "Apache-2.0", "engines": { "node": ">=16" diff --git a/packages/sdk/src/pub/types.ts b/packages/sdk/src/pub/types.ts index eeb66cc7d..5178eb8fa 100644 --- a/packages/sdk/src/pub/types.ts +++ b/packages/sdk/src/pub/types.ts @@ -69,7 +69,12 @@ export { TypedJsonParseErrorCode, TypedJson, } from '../types/util.js'; -export { jsonParse, jsonStringify } from '../types/json.js'; +export { + jsonParse, + jsonStringify, + jsonUnwrapStringify, + BigintFormatType, +} from '../types/json.js'; // These cannot be exported directly as modules because of a bug in an eslint plugin. // https://github.com/import-js/eslint-plugin-import/issues/2289. diff --git a/packages/sdk/src/types/AccountAddress.ts b/packages/sdk/src/types/AccountAddress.ts index 3824d2fd7..069e063ac 100644 --- a/packages/sdk/src/types/AccountAddress.ts +++ b/packages/sdk/src/types/AccountAddress.ts @@ -8,6 +8,11 @@ import { } from './util.js'; import { Base58String } from '../types.js'; +// IMPORTANT: +// When adding functionality to this module, it is important to not change the wrapper class, as changing this might break compatibility +// between different versions of the SDK, e.g. if a dependency exposes an API that depends on the class and a class from a different version +// of the SDK is passed. + /** * The {@linkcode TypedJsonDiscriminator} discriminator associated with {@linkcode Type} type. */ @@ -30,6 +35,16 @@ class AccountAddress { ) {} } +/** + * Unwraps {@linkcode Type} value + * + * @param value value to unwrap. + * @returns the unwrapped {@linkcode Serializable} value + */ +export function toUnwrappedJSON(value: Type): Serializable { + return toBase58(value); +} + /** * Representation of an account address, which enforces that it: * - Is a valid base58 string with version byte of 1. diff --git a/packages/sdk/src/types/BlockHash.ts b/packages/sdk/src/types/BlockHash.ts index 83f47c3cd..80587c973 100644 --- a/packages/sdk/src/types/BlockHash.ts +++ b/packages/sdk/src/types/BlockHash.ts @@ -7,6 +7,11 @@ import { makeFromTypedJson, } from './util.js'; +// IMPORTANT: +// When adding functionality to this module, it is important to not change the wrapper class, as changing this might break compatibility +// between different versions of the SDK, e.g. if a dependency exposes an API that depends on the class and a class from a different version +// of the SDK is passed. + /** * The number of bytes used to represent a block hash. */ @@ -31,6 +36,16 @@ class BlockHash { ) {} } +/** + * Unwraps {@linkcode Type} value + * + * @param value value to unwrap. + * @returns the unwrapped {@linkcode Serializable} value + */ +export function toUnwrappedJSON(value: Type): Serializable { + return toHexString(value); +} + /** * Represents a hash of a block in the chain. */ diff --git a/packages/sdk/src/types/CcdAmount.ts b/packages/sdk/src/types/CcdAmount.ts index 5c6373077..d3bdb7f52 100644 --- a/packages/sdk/src/types/CcdAmount.ts +++ b/packages/sdk/src/types/CcdAmount.ts @@ -6,6 +6,11 @@ import { makeFromTypedJson, } from './util.js'; +// IMPORTANT: +// When adding functionality to this module, it is important to not change the wrapper class, as changing this might break compatibility +// between different versions of the SDK, e.g. if a dependency exposes an API that depends on the class and a class from a different version +// of the SDK is passed. + const MICRO_CCD_PER_CCD = 1_000_000; /** * The {@linkcode TypedJsonDiscriminator} discriminator associated with {@linkcode Type} type. diff --git a/packages/sdk/src/types/ContractAddress.ts b/packages/sdk/src/types/ContractAddress.ts index 650617500..673fe423f 100644 --- a/packages/sdk/src/types/ContractAddress.ts +++ b/packages/sdk/src/types/ContractAddress.ts @@ -5,14 +5,21 @@ import { makeFromTypedJson, } from './util.js'; +// IMPORTANT: +// When adding functionality to this module, it is important to not change the wrapper class, as changing this might break compatibility +// between different versions of the SDK, e.g. if a dependency exposes an API that depends on the class and a class from a different version +// of the SDK is passed. + /** * The {@linkcode TypedJsonDiscriminator} discriminator associated with {@linkcode Type} type. */ export const JSON_DISCRIMINATOR = TypedJsonDiscriminator.ContractAddress; -export type Serializable = { index: string; subindex: string }; + +type ContractAddressLike = { index: T; subindex: T }; +export type Serializable = ContractAddressLike; /** Address of a smart contract instance. */ -class ContractAddress { +class ContractAddress implements ContractAddressLike { /** Having a private field prevents similar structured objects to be considered the same type (similar to nominal typing). */ private __type = JSON_DISCRIMINATOR; constructor( @@ -23,6 +30,19 @@ class ContractAddress { ) {} } +/** + * Unwraps {@linkcode Type} value + * + * @param value value to unwrap. + * @returns the unwrapped {@linkcode Serializable} value + */ +export function toUnwrappedJSON({ + index, + subindex, +}: Type): ContractAddressLike { + return { index, subindex }; +} + /** Address of a smart contract instance. */ export type Type = ContractAddress; diff --git a/packages/sdk/src/types/ContractEvent.ts b/packages/sdk/src/types/ContractEvent.ts index a691ee756..fcaf89578 100644 --- a/packages/sdk/src/types/ContractEvent.ts +++ b/packages/sdk/src/types/ContractEvent.ts @@ -8,6 +8,13 @@ import type { SmartContractTypeValues, } from '../types.js'; +// IMPORTANT: +// When adding functionality to this module, it is important to not change the wrapper class, as changing this might break compatibility +// between different versions of the SDK, e.g. if a dependency exposes an API that depends on the class and a class from a different version +// of the SDK is passed. + +export type Serializable = HexString; + /** * An event logged by a smart contract instance. */ @@ -20,6 +27,16 @@ class ContractEvent { ) {} } +/** + * Unwraps {@linkcode Type} value + * + * @param value value to unwrap. + * @returns the unwrapped {@linkcode Serializable} value + */ +export function toUnwrappedJSON(value: Type): Serializable { + return toHexString(value); +} + /** * An event logged by a smart contract instance. */ diff --git a/packages/sdk/src/types/ContractName.ts b/packages/sdk/src/types/ContractName.ts index 5c2812c12..5bedaba4e 100644 --- a/packages/sdk/src/types/ContractName.ts +++ b/packages/sdk/src/types/ContractName.ts @@ -6,6 +6,11 @@ import { makeFromTypedJson, } from './util.js'; +// IMPORTANT: +// When adding functionality to this module, it is important to not change the wrapper class, as changing this might break compatibility +// between different versions of the SDK, e.g. if a dependency exposes an API that depends on the class and a class from a different version +// of the SDK is passed. + /** * The {@linkcode TypedJsonDiscriminator} discriminator associated with {@linkcode Type} type. */ @@ -22,6 +27,16 @@ class ContractName { ) {} } +/** + * Unwraps {@linkcode Type} value + * + * @param value value to unwrap. + * @returns the unwrapped {@linkcode Serializable} value + */ +export function toUnwrappedJSON(value: Type): Serializable { + return toString(value); +} + /** The name of a smart contract. Note: This does _not_ including the 'init_' prefix. */ export type Type = ContractName; diff --git a/packages/sdk/src/types/CredentialRegistrationId.ts b/packages/sdk/src/types/CredentialRegistrationId.ts index 681085ac8..f9557f13e 100644 --- a/packages/sdk/src/types/CredentialRegistrationId.ts +++ b/packages/sdk/src/types/CredentialRegistrationId.ts @@ -7,6 +7,11 @@ import { makeFromTypedJson, } from './util.js'; +// IMPORTANT: +// When adding functionality to this module, it is important to not change the wrapper class, as changing this might break compatibility +// between different versions of the SDK, e.g. if a dependency exposes an API that depends on the class and a class from a different version +// of the SDK is passed. + /** * The {@linkcode TypedJsonDiscriminator} discriminator associated with {@linkcode Type} type. */ @@ -28,7 +33,7 @@ class CredentialRegistrationId { public readonly credId: string ) {} - public toJSON(): string { + public toJSON(): Serializable { return this.credId; } } diff --git a/packages/sdk/src/types/Duration.ts b/packages/sdk/src/types/Duration.ts index 289d28d5e..c85503edb 100644 --- a/packages/sdk/src/types/Duration.ts +++ b/packages/sdk/src/types/Duration.ts @@ -5,6 +5,11 @@ import { makeFromTypedJson, } from './util.js'; +// IMPORTANT: +// When adding functionality to this module, it is important to not change the wrapper class, as changing this might break compatibility +// between different versions of the SDK, e.g. if a dependency exposes an API that depends on the class and a class from a different version +// of the SDK is passed. + /** * The {@linkcode TypedJsonDiscriminator} discriminator associated with {@linkcode Type} type. */ @@ -24,6 +29,16 @@ class Duration { ) {} } +/** + * Unwraps {@linkcode Type} value + * + * @param value value to unwrap. + * @returns the unwrapped {@linkcode bigint} value + */ +export function toUnwrappedJSON(value: Type): bigint { + return value.value; +} + /** * Type representing a duration of time down to milliseconds. * Can not be negative. diff --git a/packages/sdk/src/types/Energy.ts b/packages/sdk/src/types/Energy.ts index 06e9a6d10..f2bc81a19 100644 --- a/packages/sdk/src/types/Energy.ts +++ b/packages/sdk/src/types/Energy.ts @@ -5,6 +5,11 @@ import { makeFromTypedJson, } from './util.js'; +// IMPORTANT: +// When adding functionality to this module, it is important to not change the wrapper class, as changing this might break compatibility +// between different versions of the SDK, e.g. if a dependency exposes an API that depends on the class and a class from a different version +// of the SDK is passed. + /** * The {@linkcode TypedJsonDiscriminator} discriminator associated with {@linkcode Type} type. */ @@ -25,6 +30,16 @@ class Energy { ) {} } +/** + * Unwraps {@linkcode Type} value + * + * @param value value to unwrap. + * @returns the unwrapped {@linkcode bigint} value + */ +export function toUnwrappedJSON(value: Type): bigint { + return value.value; +} + /** Energy measure. Used as part of cost calculations for transactions. */ export type Type = Energy; diff --git a/packages/sdk/src/types/EntrypointName.ts b/packages/sdk/src/types/EntrypointName.ts index 95a121373..32c6dadec 100644 --- a/packages/sdk/src/types/EntrypointName.ts +++ b/packages/sdk/src/types/EntrypointName.ts @@ -5,6 +5,11 @@ import { makeFromTypedJson, } from './util.js'; +// IMPORTANT: +// When adding functionality to this module, it is important to not change the wrapper class, as changing this might break compatibility +// between different versions of the SDK, e.g. if a dependency exposes an API that depends on the class and a class from a different version +// of the SDK is passed. + /** * The {@linkcode TypedJsonDiscriminator} discriminator associated with {@linkcode Type} type. */ @@ -28,6 +33,16 @@ class EntrypointName { ) {} } +/** + * Unwraps {@linkcode Type} value + * + * @param value value to unwrap. + * @returns the unwrapped {@linkcode Serializable} value + */ +export function toUnwrappedJSON(value: Type): S { + return toString(value); +} + /** * Type representing an entrypoint of a smart contract. */ diff --git a/packages/sdk/src/types/InitName.ts b/packages/sdk/src/types/InitName.ts index a50e46fea..c2ca20d60 100644 --- a/packages/sdk/src/types/InitName.ts +++ b/packages/sdk/src/types/InitName.ts @@ -7,6 +7,11 @@ import { makeFromTypedJson, } from './util.js'; +// IMPORTANT: +// When adding functionality to this module, it is important to not change the wrapper class, as changing this might break compatibility +// between different versions of the SDK, e.g. if a dependency exposes an API that depends on the class and a class from a different version +// of the SDK is passed. + /** * The {@linkcode TypedJsonDiscriminator} discriminator associated with {@linkcode Type} type. */ @@ -27,6 +32,16 @@ class InitName { ) {} } +/** + * Unwraps {@linkcode Type} value + * + * @param value value to unwrap. + * @returns the unwrapped {@linkcode Serializable} value + */ +export function toUnwrappedJSON(value: Type): Serializable { + return toString(value); +} + /** The name of an init-function for a smart contract. Note: This is of the form 'init_'. */ export type Type = InitName; diff --git a/packages/sdk/src/types/ModuleReference.ts b/packages/sdk/src/types/ModuleReference.ts index 6d4d058ca..a5c1bd556 100644 --- a/packages/sdk/src/types/ModuleReference.ts +++ b/packages/sdk/src/types/ModuleReference.ts @@ -8,6 +8,11 @@ import { makeFromTypedJson, } from './util.js'; +// IMPORTANT: +// When adding functionality to this module, it is important to not change the wrapper class, as changing this might break compatibility +// between different versions of the SDK, e.g. if a dependency exposes an API that depends on the class and a class from a different version +// of the SDK is passed. + /** * The number of bytes used to represent a block hash. */ @@ -31,13 +36,23 @@ class ModuleReference { public readonly decodedModuleRef: Uint8Array ) {} - public toJSON(): string { + public toJSON(): Serializable { return packBufferWithWord32Length(this.decodedModuleRef).toString( 'hex' ); } } +/** + * Unwraps {@linkcode Type} value + * + * @param value value to unwrap. + * @returns the unwrapped {@linkcode Serializable} value + */ +export function toUnwrappedJSON(value: Type): Serializable { + return value.toJSON(); +} + /** * Reference to a smart contract module. */ diff --git a/packages/sdk/src/types/Parameter.ts b/packages/sdk/src/types/Parameter.ts index 097253051..1b43c0e1e 100644 --- a/packages/sdk/src/types/Parameter.ts +++ b/packages/sdk/src/types/Parameter.ts @@ -10,6 +10,11 @@ import { makeFromTypedJson, } from './util.js'; +// IMPORTANT: +// When adding functionality to this module, it is important to not change the wrapper class, as changing this might break compatibility +// between different versions of the SDK, e.g. if a dependency exposes an API that depends on the class and a class from a different version +// of the SDK is passed. + /** * The {@linkcode TypedJsonDiscriminator} discriminator associated with {@linkcode Type} type. */ @@ -26,6 +31,16 @@ class Parameter { ) {} } +/** + * Unwraps {@linkcode Type} value + * + * @param value value to unwrap. + * @returns the unwrapped {@linkcode Serializable} value + */ +export function toUnwrappedJSON(value: Type): Serializable { + return toHexString(value); +} + /** Parameter for a smart contract entrypoint. */ export type Type = Parameter; diff --git a/packages/sdk/src/types/ReceiveName.ts b/packages/sdk/src/types/ReceiveName.ts index 5861fea3d..384ad93d7 100644 --- a/packages/sdk/src/types/ReceiveName.ts +++ b/packages/sdk/src/types/ReceiveName.ts @@ -8,6 +8,11 @@ import { makeFromTypedJson, } from './util.js'; +// IMPORTANT: +// When adding functionality to this module, it is important to not change the wrapper class, as changing this might break compatibility +// between different versions of the SDK, e.g. if a dependency exposes an API that depends on the class and a class from a different version +// of the SDK is passed. + /** * The {@linkcode TypedJsonDiscriminator} discriminator associated with {@linkcode Type} type. */ @@ -30,6 +35,16 @@ class ReceiveName { ) {} } +/** + * Unwraps {@linkcode Type} value + * + * @param value value to unwrap. + * @returns the unwrapped {@linkcode Serializable} value + */ +export function toUnwrappedJSON(value: Type): Serializable { + return toString(value); +} + /** * Represents a receive-function in a smart contract module. * A value of this type is assumed to be a valid receive name which means: diff --git a/packages/sdk/src/types/ReturnValue.ts b/packages/sdk/src/types/ReturnValue.ts index 0394113ed..2910b13e4 100644 --- a/packages/sdk/src/types/ReturnValue.ts +++ b/packages/sdk/src/types/ReturnValue.ts @@ -12,6 +12,11 @@ import { makeFromTypedJson, } from './util.js'; +// IMPORTANT: +// When adding functionality to this module, it is important to not change the wrapper class, as changing this might break compatibility +// between different versions of the SDK, e.g. if a dependency exposes an API that depends on the class and a class from a different version +// of the SDK is passed. + /** * The {@linkcode TypedJsonDiscriminator} discriminator associated with {@linkcode Type} type. */ @@ -28,6 +33,16 @@ class ReturnValue { ) {} } +/** + * Unwraps {@linkcode Type} value + * + * @param value value to unwrap. + * @returns the unwrapped {@linkcode Serializable} value + */ +export function toUnwrappedJSON(value: Type): Serializable { + return toHexString(value); +} + /** Return value from invoking a smart contract entrypoint. */ export type Type = ReturnValue; diff --git a/packages/sdk/src/types/SequenceNumber.ts b/packages/sdk/src/types/SequenceNumber.ts index 300c322be..5b7f78444 100644 --- a/packages/sdk/src/types/SequenceNumber.ts +++ b/packages/sdk/src/types/SequenceNumber.ts @@ -5,6 +5,11 @@ import { makeFromTypedJson, } from './util.js'; +// IMPORTANT: +// When adding functionality to this module, it is important to not change the wrapper class, as changing this might break compatibility +// between different versions of the SDK, e.g. if a dependency exposes an API that depends on the class and a class from a different version +// of the SDK is passed. + /** * The {@linkcode TypedJsonDiscriminator} discriminator associated with {@linkcode Type} type. */ @@ -21,6 +26,16 @@ class SequenceNumber { ) {} } +/** + * Unwraps {@linkcode Type} value + * + * @param value value to unwrap. + * @returns the unwrapped {@linkcode bigint} value + */ +export function toUnwrappedJSON(value: Type): bigint { + return value.value; +} + /** A transaction sequence number. (Formerly refered as Nonce) */ export type Type = SequenceNumber; diff --git a/packages/sdk/src/types/Timestamp.ts b/packages/sdk/src/types/Timestamp.ts index daceb757b..cb479edf8 100644 --- a/packages/sdk/src/types/Timestamp.ts +++ b/packages/sdk/src/types/Timestamp.ts @@ -5,6 +5,11 @@ import { makeFromTypedJson, } from './util.js'; +// IMPORTANT: +// When adding functionality to this module, it is important to not change the wrapper class, as changing this might break compatibility +// between different versions of the SDK, e.g. if a dependency exposes an API that depends on the class and a class from a different version +// of the SDK is passed. + /** * The {@linkcode TypedJsonDiscriminator} discriminator associated with {@linkcode Type} type. */ @@ -21,6 +26,16 @@ class Timestamp { ) {} } +/** + * Unwraps {@linkcode Type} value + * + * @param value value to unwrap. + * @returns the unwrapped {@linkcode bigint} value + */ +export function toUnwrappedJSON(value: Type): bigint { + return value.value; +} + /** Represents a timestamp. */ export type Type = Timestamp; diff --git a/packages/sdk/src/types/TransactionExpiry.ts b/packages/sdk/src/types/TransactionExpiry.ts index 4f43ed384..50e9b240e 100644 --- a/packages/sdk/src/types/TransactionExpiry.ts +++ b/packages/sdk/src/types/TransactionExpiry.ts @@ -6,6 +6,11 @@ import { makeFromTypedJson, } from './util.js'; +// IMPORTANT: +// When adding functionality to this module, it is important to not change the wrapper class, as changing this might break compatibility +// between different versions of the SDK, e.g. if a dependency exposes an API that depends on the class and a class from a different version +// of the SDK is passed. + /** * The {@linkcode TypedJsonDiscriminator} discriminator associated with {@linkcode Type} type. */ diff --git a/packages/sdk/src/types/TransactionHash.ts b/packages/sdk/src/types/TransactionHash.ts index 6f904ac81..cc875d967 100644 --- a/packages/sdk/src/types/TransactionHash.ts +++ b/packages/sdk/src/types/TransactionHash.ts @@ -7,6 +7,11 @@ import { makeFromTypedJson, } from './util.js'; +// IMPORTANT: +// When adding functionality to this module, it is important to not change the wrapper class, as changing this might break compatibility +// between different versions of the SDK, e.g. if a dependency exposes an API that depends on the class and a class from a different version +// of the SDK is passed. + /** * The {@linkcode TypedJsonDiscriminator} discriminator associated with {@linkcode Type} type. */ @@ -28,6 +33,16 @@ class TransactionHash { ) {} } +/** + * Unwraps {@linkcode Type} value + * + * @param value value to unwrap. + * @returns the unwrapped {@linkcode Serializable} value + */ +export function toUnwrappedJSON(value: Type): Serializable { + return toHexString(value); +} + /** Hash of a transaction. */ export type Type = TransactionHash; diff --git a/packages/sdk/src/types/json.ts b/packages/sdk/src/types/json.ts index 610c10137..6b09af52d 100644 --- a/packages/sdk/src/types/json.ts +++ b/packages/sdk/src/types/json.ts @@ -22,6 +22,7 @@ import { JSON_DISCRIMINATOR as DATA_BLOB_DISCRIMINATOR, } from './DataBlob.js'; import { isTypedJsonCandidate } from './util.js'; +import JSONBig from 'json-bigint'; function reviveConcordiumTypes(value: unknown) { if (isTypedJsonCandidate(value)) { @@ -85,7 +86,7 @@ export function jsonParse( } /** - * Replaces values of concordium domain types with values that can be revived into their original types. + * Replaces values of concordium domain types with values that can be revived into their original types. Returns undefined if type cannot be matched. */ function transformConcordiumType(value: unknown): unknown | undefined { switch (true) { @@ -136,6 +137,60 @@ function transformConcordiumType(value: unknown): unknown | undefined { return undefined; } +/** + * Replaces values of concordium domain types with their unwrapped values. Returns undefined if type cannot be matched. + */ +function unwrapConcordiumType(value: unknown): unknown | undefined { + switch (true) { + case AccountAddress.instanceOf(value): + return AccountAddress.toUnwrappedJSON(value as AccountAddress.Type); + case BlockHash.instanceOf(value): + return BlockHash.toUnwrappedJSON(value as BlockHash.Type); + case CcdAmount.instanceOf(value): + return (value as CcdAmount.Type).toJSON(); + case ContractAddress.instanceOf(value): + return ContractAddress.toUnwrappedJSON( + value as ContractAddress.Type + ); + case ContractName.instanceOf(value): + return ContractName.toUnwrappedJSON(value as ContractName.Type); + case CredentialRegistrationId.instanceOf(value): + return (value as CredentialRegistrationId.Type).toJSON(); + case value instanceof DataBlob: + return (value as DataBlob).toJSON(); + case Duration.instanceOf(value): + return Duration.toUnwrappedJSON(value as Duration.Type); + case Energy.instanceOf(value): + return Energy.toUnwrappedJSON(value as Energy.Type); + case EntrypointName.instanceOf(value): + return EntrypointName.toUnwrappedJSON(value as EntrypointName.Type); + case InitName.instanceOf(value): + return InitName.toUnwrappedJSON(value as InitName.Type); + case ModuleReference.instanceOf(value): + return ModuleReference.toUnwrappedJSON( + value as ModuleReference.Type + ); + case Parameter.instanceOf(value): + return Parameter.toUnwrappedJSON(value as Parameter.Type); + case ReceiveName.instanceOf(value): + return ReceiveName.toUnwrappedJSON(value as ReceiveName.Type); + case ReturnValue.instanceOf(value): + return ReturnValue.toUnwrappedJSON(value as ReturnValue.Type); + case SequenceNumber.instanceOf(value): + return SequenceNumber.toUnwrappedJSON(value as SequenceNumber.Type); + case Timestamp.instanceOf(value): + return Timestamp.toUnwrappedJSON(value as Timestamp.Type); + case TransactionExpiry.instanceOf(value): + return (value as TransactionExpiry.Type).toJSON(); + case TransactionHash.instanceOf(value): + return TransactionHash.toUnwrappedJSON( + value as TransactionHash.Type + ); + } + + return undefined; +} + type ReplacerFun = (this: any, key: string, value: any) => any; function ccdTypesReplacer(this: any, key: string, value: any): any { @@ -143,8 +198,14 @@ function ccdTypesReplacer(this: any, key: string, value: any): any { return transformConcordiumType(rawValue) ?? value; } +function ccdUnwrapReplacer(this: any, key: string, value: any): any { + const rawValue = this[key]; + return unwrapConcordiumType(rawValue) ?? value; +} + /** * Stringify, which ensures concordium domain types are stringified in a restorable fashion. + * This should be used if you want to be able to restore the concordium domain types in the JSON to its original types. * * @param value A JavaScript value, usually an object or array, to be converted. * @param replacer A function that transforms the results. @@ -161,3 +222,78 @@ export function jsonStringify( } return JSON.stringify(input, replacerFunction, space); } + +/** + * Describes how bigints encountered in {@linkcode jsonUnwrapStringify} are handled by default. + */ +export const enum BigintFormatType { + /** Use 'json-bigint' to safely convert `bigint`s to integers */ + Integer, + /** Convert `bigint`s to strings */ + String, + /** Do nothing, i.e. must be handled manually in replacer function. */ + None, +} + +/** + * Stringify, which ensures concordium domain types are unwrapped to their inner type before stringified. + * This should be used if you want to manually deserialize the inner property values, as the serialization is irreversible. + * + * @param value A JavaScript value, usually an object or array, to be converted. + * @param bigintFormat Determines how to handle bigints. Can be set to either: + * - `BigintFormatType.Number`: uses 'json-bigint to safely serialize, + * - `BigintFormatType.String`: converts `bigint` to strings + * - `BigintFormatType.None`: must be taken care of manually, e.g. in replacer function. + * Defaults to BigintFormatType.None + * @param replacer A function that transforms the results. + * This overrides `bigintFormat`, and will also run on primitive values passed as `value.` + * @param space Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read. + * + * @example + * jsonUnwrapStringify(100n) => throws `TypeError`, as bigints cannot be serialized. + * jsonUnwrapStringify(100n, BigintFormatType.None) => throws `TypeError` + * jsonUnwrapStringify(100n, BigintFormatType.None, (_key, value) => 'replaced') => '"replaced"' + * + * jsonUnwrapStringify(100n, BigintFormatType.Number) => '100' + * jsonUnwrapStringify(100n, BigintFormatType.Number, (_key, value) => -value) => '-100' // runs both replacer and bigintFormat + * jsonUnwrapStringify(100n, BigintFormatType.Number, (_key, value) => 'replaced') => '"replaced"' // replacer takes precedence + * + * jsonUnwrapStringify(100n, BigintFormatType.String) => '"100"' + * jsonUnwrapStringify(100n, BigintFormatType.String, (_key, value) => -value) => '"-100"' // runs both replacer and bigintFormat + * jsonUnwrapStringify(100n, BigintFormatType.String, (_key, value) => 10) => '10' // replacer takes precedence + */ +export function jsonUnwrapStringify( + input: any, + bigintFormat = BigintFormatType.None, + replacer?: ReplacerFun, + space?: string | number +): string { + function replaceBigintValue(value: any): any { + switch (bigintFormat) { + case BigintFormatType.String: + if (typeof value === 'bigint') { + return value.toString(); + } + default: + return value; + } + } + + function replacerFunction(this: any, key: string, value: any) { + let replaced = ccdUnwrapReplacer.call(this, key, value); + replaced = replacer?.call(this, key, replaced) ?? replaced; + return replaceBigintValue(replaced); + } + + let replaced = input; + if (typeof input !== 'object') { + replaced = replacer?.call(replaced, '', replaced) ?? replaced; + replaced = replaceBigintValue(replaced); + } + + const stringify = + bigintFormat === BigintFormatType.Integer + ? JSONBig.stringify + : JSON.stringify; + return stringify(replaced, replacerFunction, space); +} diff --git a/packages/sdk/test/ci/types/json.test.ts b/packages/sdk/test/ci/types/json.test.ts index 149649089..7615b813f 100644 --- a/packages/sdk/test/ci/types/json.test.ts +++ b/packages/sdk/test/ci/types/json.test.ts @@ -18,8 +18,10 @@ import { TransactionExpiry, ModuleReference, DataBlob, + jsonUnwrapStringify, jsonStringify, jsonParse, + BigintFormatType, } from '../../../src/pub/types.js'; describe('JSON ID test', () => { @@ -71,7 +73,7 @@ describe('JSON ID test', () => { }); }); -describe('jsonStringify', () => { +describe(jsonStringify, () => { test('Throws on circular reference', () => { const obj = {}; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -85,3 +87,276 @@ describe('jsonStringify', () => { expect(() => jsonStringify([other, other])).not.toThrow(); }); }); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const testBigintReplacer = (_k: any, v: any) => + typeof v === 'bigint' ? 'replaced' : v; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const unsafeReplacer = (_: any, v: any) => + typeof v === 'bigint' ? Number(v) : v; + +describe(jsonUnwrapStringify, () => { + const t = 100n; + + test('Serializes bigint values as expected', () => { + expect(jsonUnwrapStringify(t, BigintFormatType.Integer)).toEqual('100'); + expect(jsonUnwrapStringify(t, BigintFormatType.String)).toEqual( + '"100"' + ); + expect( + jsonUnwrapStringify(t, BigintFormatType.None, testBigintReplacer) + ).toEqual('"replaced"'); + + // Test for numbers bigger than `Number.MAX_SAFE_INTEGER` + const unsafeNumber = SequenceNumber.create(9007199254740997n); + const unsafeExpected = '9007199254740997'; + expect( + jsonUnwrapStringify(unsafeNumber, undefined, unsafeReplacer) + ).not.toEqual(unsafeExpected); + expect( + jsonUnwrapStringify(unsafeNumber, BigintFormatType.Integer) + ).toEqual(unsafeExpected); + }); + + test('Throws `TypeError` on serialize bigint', () => { + expect(() => jsonUnwrapStringify(t)).toThrowError(TypeError); + }); + + test('Serializes nested bigint (arrays) values as expected', () => { + const t = [100n, 200n]; + expect(jsonUnwrapStringify(t, BigintFormatType.Integer)).toEqual( + '[100,200]' + ); + expect(jsonUnwrapStringify(t, BigintFormatType.String)).toEqual( + '["100","200"]' + ); + expect( + jsonUnwrapStringify(t, BigintFormatType.None, testBigintReplacer) + ).toEqual('["replaced","replaced"]'); + }); + + test('Serializes nested bigint (objects) values as expected', () => { + const t = { a: 100n, b: 200n }; + expect(jsonUnwrapStringify(t, BigintFormatType.Integer)).toEqual( + '{"a":100,"b":200}' + ); + expect(jsonUnwrapStringify(t, BigintFormatType.String)).toEqual( + '{"a":"100","b":"200"}' + ); + expect( + jsonUnwrapStringify(t, BigintFormatType.None, testBigintReplacer) + ).toEqual('{"a":"replaced","b":"replaced"}'); + }); + + test('Replacer overrides bigint default', () => { + expect( + jsonUnwrapStringify(t, BigintFormatType.Integer, testBigintReplacer) + ).toEqual('"replaced"'); + expect( + jsonUnwrapStringify(t, BigintFormatType.String, testBigintReplacer) + ).toEqual('"replaced"'); + }); +}); + +describe('ContractName', () => { + test('Unwraps as expected', () => { + const t = ContractName.fromString('some-name'); + const e = '"some-name"'; + + expect(jsonUnwrapStringify(t, BigintFormatType.Integer)).toEqual(e); + }); +}); + +describe('InitName', () => { + test('Unwraps as expected', () => { + const t = InitName.fromString('init_some-name'); + const e = '"init_some-name"'; + + expect(jsonUnwrapStringify(t, BigintFormatType.Integer)).toEqual(e); + }); +}); + +describe('ReceiveName', () => { + test('Unwraps as expected', () => { + const t = ReceiveName.fromString('some_name.test'); + const e = '"some_name.test"'; + + expect(jsonUnwrapStringify(t, BigintFormatType.Integer)).toEqual(e); + }); +}); + +describe('EntrypointName', () => { + test('Unwraps as expected', () => { + const t = EntrypointName.fromString('some_name.test'); + const e = '"some_name.test"'; + + expect(jsonUnwrapStringify(t, BigintFormatType.Integer)).toEqual(e); + }); +}); + +describe('TransactionExpiry', () => { + test('Unwraps as expected', () => { + const t = TransactionExpiry.fromEpochSeconds(300); + const e = '300'; + + expect(jsonUnwrapStringify(t, BigintFormatType.Integer)).toEqual(e); + }); +}); + +describe('CcdAmount', () => { + test('Unwraps as expected', () => { + let t = CcdAmount.fromMicroCcd(300); + let e = '"300"'; + + expect(jsonUnwrapStringify(t, BigintFormatType.Integer)).toEqual(e); + + // Test for numbers bigger than Number.MAX_SAFE_INTEGER + t = CcdAmount.fromMicroCcd(9007199254740997n); + e = '"9007199254740997"'; + expect(jsonUnwrapStringify(t, BigintFormatType.Integer)).toEqual(e); + }); +}); + +describe('SequenceNumber', () => { + test('Unwraps as expected', () => { + const t = SequenceNumber.create(300); + const e = '300'; + expect(jsonUnwrapStringify(t, BigintFormatType.Integer)).toEqual(e); + }); +}); + +describe('Energy', () => { + test('Unwraps as expected', () => { + let t = Energy.create(300); + let e = '300'; + + expect(jsonUnwrapStringify(t, BigintFormatType.Integer)).toEqual(e); + + // Test for numbers bigger than Number.MAX_SAFE_INTEGER + t = Energy.create(9007199254740997n); + e = '9007199254740997'; + expect(jsonUnwrapStringify(t, BigintFormatType.Integer)).toEqual(e); + }); +}); + +describe('Timestamp', () => { + test('Unwraps as expected', () => { + let t = Timestamp.fromMillis(300); + let e = '300'; + expect(jsonUnwrapStringify(t, BigintFormatType.Integer)).toEqual(e); + + // Test for numbers bigger than Number.MAX_SAFE_INTEGER + t = Timestamp.fromMillis(9007199254740997n); + e = '9007199254740997'; + expect(jsonUnwrapStringify(t, BigintFormatType.Integer)).toEqual(e); + }); +}); + +describe('Duration', () => { + test('Unwraps as expected', () => { + let t = Duration.fromMillis(300); + let e = '300'; + expect(jsonUnwrapStringify(t, BigintFormatType.Integer)).toEqual(e); + + // Test for numbers bigger than Number.MAX_SAFE_INTEGER + t = Duration.fromMillis(9007199254740997n); + e = '9007199254740997'; + expect(jsonUnwrapStringify(t, BigintFormatType.Integer)).toEqual(e); + }); +}); + +describe('ContractAddress', () => { + test('Unwraps as expected', () => { + let t = ContractAddress.create(100, 10); + let e = '{"index":100,"subindex":10}'; + expect(jsonUnwrapStringify(t, BigintFormatType.Integer)).toEqual(e); + expect(jsonUnwrapStringify(t, BigintFormatType.String)).toEqual( + '{"index":"100","subindex":"10"}' + ); + + // Test for numbers bigger than Number.MAX_SAFE_INTEGER + t = ContractAddress.create(9007199254740997n, 10); + e = '{"index":9007199254740997,"subindex":10}'; + expect(jsonUnwrapStringify(t, BigintFormatType.Integer)).toEqual(e); + + t = ContractAddress.create(9007199254740997n, 109007199254740997n); + e = '{"index":9007199254740997,"subindex":109007199254740997}'; + expect(jsonUnwrapStringify(t, BigintFormatType.Integer)).toEqual(e); + }); +}); + +describe('Parameter', () => { + test('Unwraps as expected', () => { + const t = Parameter.fromHexString('000102'); + const e = '"000102"'; + expect(jsonUnwrapStringify(t, BigintFormatType.Integer)).toEqual(e); + }); +}); + +describe('TransactionHash', () => { + test('Unwraps as expected', () => { + const v = + '1a17008f7944a5fd11a665a864266fb2d76794e754986c367455a4937fd3a66b'; + const t = TransactionHash.fromHexString(v); + const e = `"${v}"`; + expect(jsonUnwrapStringify(t, BigintFormatType.Integer)).toEqual(e); + }); +}); + +describe('BlockHash', () => { + test('Unwraps as expected', () => { + const v = + '1a17008f7944a5fd11a665a864266fb2d76794e754986c367455a4937fd3a66b'; + const t = BlockHash.fromHexString(v); + const e = `"${v}"`; + expect(jsonUnwrapStringify(t, BigintFormatType.Integer)).toEqual(e); + }); +}); + +describe('ReturnValue', () => { + test('Unwraps as expected', () => { + const t = ReturnValue.fromHexString('000102'); + const e = '"000102"'; + expect(jsonUnwrapStringify(t, BigintFormatType.Integer)).toEqual(e); + }); +}); + +describe('ModuleReference', () => { + test('Unwraps as expected', () => { + const v = + '5d99b6dfa7ba9dc0cac8626754985500d51d6d06829210748b3fd24fa30cde4a'; + const t = ModuleReference.fromHexString(v); + const e = `"00000020${v}"`; // is prefixed with 4 byte length + expect(jsonUnwrapStringify(t, BigintFormatType.Integer)).toEqual(e); + }); +}); + +describe('CredentialRegistrationId', () => { + test('Unwraps as expected', () => { + const v = + '83e4b29e1e2582a6f1dcc93bf2610ce6b0a6ba89c8f03e661f403b4c2e055d3adb80d071c2723530926bb8aed3ed52b1'; + const t = CredentialRegistrationId.fromHexString(v); + const e = `"${v}"`; + expect(jsonUnwrapStringify(t, BigintFormatType.Integer)).toEqual(e); + }); +}); + +describe('DataBlob', () => { + test('Unwraps as expected', () => { + const v = + '83e4b29e1e2582a6f1dcc93bf2610ce6b0a6ba89c8f03e661f403b4c2e055d3adb80d071c2723530926bb8aed3ed52b1'; + const t = new DataBlob(Buffer.from(v, 'hex')); + const e = `"0030${v}"`; // Is prefixed with 2 byte length + expect(jsonUnwrapStringify(t, BigintFormatType.Integer)).toEqual(e); + }); +}); + +describe('AccountAddress', () => { + test('Unwraps as expected', () => { + const v = '4owvMHZSKsPW8QGYUEWSdgqxfoPBh3ZwPameBV46pSvmeHDkEe'; + const t = AccountAddress.fromBase58(v); + const e = `"${v}"`; // Is prefixed with 2 byte length + expect(jsonUnwrapStringify(t, BigintFormatType.Integer)).toEqual(e); + }); +});