From d36bce6db439b958c37abf10a159df66072ac632 Mon Sep 17 00:00:00 2001 From: balintklement Date: Mon, 24 May 2021 19:22:47 +0200 Subject: [PATCH] Add mint field to transaction and functions for parsing Int64 --- src/errors/invalidDataReason.ts | 22 ++++++------- src/parsing/transaction.ts | 9 +++++- src/types/internal.ts | 5 +++ src/types/public.ts | 33 ++++++++++++++++++- src/utils/parse.ts | 56 ++++++++++++++++++++++++++++++++- 5 files changed, 111 insertions(+), 14 deletions(-) diff --git a/src/errors/invalidDataReason.ts b/src/errors/invalidDataReason.ts index 547383f1..b9dec2e1 100644 --- a/src/errors/invalidDataReason.ts +++ b/src/errors/invalidDataReason.ts @@ -18,17 +18,17 @@ export enum InvalidDataReason { OUTPUTS_NOT_ARRAY = "outputs not an array", - OUTPUT_INVALID_AMOUNT = "invalid amount in an output", - OUTPUT_INVALID_POLICY_NAME = "invalid policy id in the token bundle in an output ", - OUTPUT_INVALID_TOKEN_BUNDLE_NOT_ARRAY = "invalid token bundle in an output - asset groups not an array", - OUTPUT_INVALID_TOKEN_BUNDLE_TOO_LARGE = "invalid token bundle in an output - too many asset groups", - OUTPUT_INVALID_TOKEN_BUNDLE_ORDERING = "invalid token bundle in an output - incorrect ordering of asset groups", - OUTPUT_INVALID_TOKEN_BUNDLE_NOT_UNIQUE = "invalid token bundle in an output - policyIds not unique", - OUTPUT_INVALID_ASSET_NAME = "invalid asset name in the token bundle in an output", - OUTPUT_INVALID_ASSET_GROUP_NOT_ARRAY = "invalid asset group in the token bundle in an output - tokens not an array", - OUTPUT_INVALID_ASSET_GROUP_TOO_LARGE = "invalid asset group in the token bundle in an output - too many tokens", - OUTPUT_INVALID_ASSET_GROUP_ORDERING = "invalid asset group in the token bundle in an output - incorrect ordering of tokens", - OUTPUT_INVALID_ASSET_GROUP_NOT_UNIQUE = "invalid asset group in the token bundle in an output - token names not unique", + OUTPUT_INVALID_AMOUNT = "invalid amount in an output (or mint field)", + OUTPUT_INVALID_POLICY_NAME = "invalid policy id in the token bundle in an output (or mint field)", + OUTPUT_INVALID_TOKEN_BUNDLE_NOT_ARRAY = "invalid token bundle in an output (or mint field) - asset groups not an array", + OUTPUT_INVALID_TOKEN_BUNDLE_TOO_LARGE = "invalid token bundle in an output (or mint field) - too many asset groups", + OUTPUT_INVALID_TOKEN_BUNDLE_ORDERING = "invalid token bundle in an output (or mint field) - incorrect ordering of asset groups", + OUTPUT_INVALID_TOKEN_BUNDLE_NOT_UNIQUE = "invalid token bundle in an output (or mint field) - policyIds not unique", + OUTPUT_INVALID_ASSET_NAME = "invalid asset name in the token bundle in an output (or mint field)", + OUTPUT_INVALID_ASSET_GROUP_NOT_ARRAY = "invalid asset group in the token bundle in an output (or mint field) - tokens not an array", + OUTPUT_INVALID_ASSET_GROUP_TOO_LARGE = "invalid asset group in the token bundle in an output (or mint field) - too many tokens", + OUTPUT_INVALID_ASSET_GROUP_ORDERING = "invalid asset group in the token bundle in an output (or mint field) - incorrect ordering of tokens", + OUTPUT_INVALID_ASSET_GROUP_NOT_UNIQUE = "invalid asset group in the token bundle in an output (or mint field) - token names not unique", OUTPUT_INVALID_ADDRESS = "invalid address in an output", diff --git a/src/parsing/transaction.ts b/src/parsing/transaction.ts index bdfd01ca..bf34eec8 100644 --- a/src/parsing/transaction.ts +++ b/src/parsing/transaction.ts @@ -24,7 +24,7 @@ import { } from "../types/public" import { unreachable } from "../utils/assert" import { isArray, parseBIP32Path, validate } from "../utils/parse" -import { parseHexString, parseHexStringOfLength, parseUint32_t, parseUint64_str } from "../utils/parse" +import { parseHexString, parseHexStringOfLength, parseInt64_str, parseUint32_t, parseUint64_str } from "../utils/parse" import { parseAddress } from "./address" import { parseCertificate } from "./certificate" import { ASSET_GROUPS_MAX, MAX_LOVELACE_SUPPLY_STR, TOKENS_IN_GROUP_MAX } from "./constants" @@ -129,6 +129,12 @@ export function parseTransaction(tx: Transaction): ParsedTransaction { ? null : parseUint64_str(tx.validityIntervalStart, {}, InvalidDataReason.VALIDITY_INTERVAL_START_INVALID) + // mint instructions + validate(isArray(tx.mintInstructions ?? []), InvalidDataReason.OUTPUT_INVALID_TOKEN_BUNDLE_NOT_ARRAY) + const mintInstructions = tx.mintInstructions == null + ? null + : parseTokenBundle(tx.mintInstructions, parseInt64_str) + return { network, inputs, @@ -139,6 +145,7 @@ export function parseTransaction(tx: Transaction): ParsedTransaction { withdrawals, certificates, fee, + mintInstructions, } } diff --git a/src/types/internal.ts b/src/types/internal.ts index a5788a92..948625d6 100644 --- a/src/types/internal.ts +++ b/src/types/internal.ts @@ -14,6 +14,10 @@ export type Uint32_t = number & { __type: 'uint32_t' } export type Uint16_t = number & { __type: 'uint16_t' } export type Uint8_t = number & { __type: 'uint8_t' } +export type Int64_str = string & { __type: 'int64_t' } +export type _Int64_num = number & { __type: 'int64_t' } +export type _Int64_bigint = bigint & { __type: 'int64_t' } + // Reexport blockchain spec export { AddressType, CertificateType, RelayType, PoolKeyType, PoolOwnerType, PoolRewardAccountType, TransactionSigningMode, TxAuxiliaryDataType, TxOutputDestinationType } export { Version, DeviceCompatibility } from './public' @@ -95,6 +99,7 @@ export type ParsedTransaction = { withdrawals: ParsedWithdrawal[] auxiliaryData: ParsedTxAuxiliaryData | null validityIntervalStart: Uint64_str | null + mintInstructions: Array> | null } export type ParsedSigningRequest = { diff --git a/src/types/public.ts b/src/types/public.ts index e9c762c5..fb5cf160 100644 --- a/src/types/public.ts +++ b/src/types/public.ts @@ -715,6 +715,33 @@ export type Withdrawal = { amount: bigint_like, }; +/** + * Describes a single token to be minted or burned during the transaction + * @category ~Mary~ --> todo? + * @see [[MintAssetGroup]] + */ +export type MintToken = { + assetNameHex: string, + /** Note: device does not know the number of decimal places the token uses */ + amount: bigint_like, +}; + +/** + * Describes a group of assets defined in the same policy to be minted or burned during the transaction + * @category ~Mary~ --> todo? + * @see [[Transaction]] + */ +export type MintAssetGroup = { + policyIdHex: string, + /** + * The keys must be sorted lowest value to highest to reflect a valid canonical CBOR. + * The sorting rules (as described in the [CBOR RFC](https://datatracker.ietf.org/doc/html/rfc7049#section-3.9)) are: + * * if two keys have different lengths, the shorter one sorts earlier; + * * if two keys have the same length, the one with the lower value in lexical order sorts earlier. + */ + tokens: Array, +}; + /** * Device app flags * @category Basic types @@ -995,7 +1022,11 @@ export type Transaction = { * Validity start (block height) if any. * Transaction becomes valid only starting from this block height. */ - validityIntervalStart?: bigint_like | null + validityIntervalStart?: bigint_like | null, + /** + * Mint or burn instructions (if any) + */ + mintInstructions?: Array | null, } /** diff --git a/src/utils/parse.ts b/src/utils/parse.ts index 3909cdd2..b2307b20 100644 --- a/src/utils/parse.ts +++ b/src/utils/parse.ts @@ -1,8 +1,10 @@ import { InvalidData } from "../errors" import type { InvalidDataReason } from "../errors/index" -import type { _Uint64_bigint, _Uint64_num, FixlenHexString, HexString, Uint8_t, Uint16_t, Uint32_t, Uint64_str, ValidBIP32Path, VarlenAsciiString } from "../types/internal" +import type { _Int64_bigint, _Int64_num, _Uint64_bigint, _Uint64_num, FixlenHexString, HexString, Int64_str, Uint8_t, Uint16_t, Uint32_t, Uint64_str, ValidBIP32Path, VarlenAsciiString } from "../types/internal" export const MAX_UINT_64_STR = "18446744073709551615" +export const MIN_INT_64_STR = "-9223372036854775808" +export const MAX_INT_64_STR = "9223372036854775807" export const isString = (data: unknown): data is string => typeof data === "string" @@ -64,6 +66,41 @@ export const isUintStr = (data: unknown, constraints: { min?: string, max?: stri ) } + +export const isInt64str = (data: unknown): data is Int64_str => + isIntStr(data, {}) + +export const isInt64Number = (data: unknown): data is _Int64_num => + isInteger(data) && data >= Number.MIN_SAFE_INTEGER && data <= Number.MAX_SAFE_INTEGER + +export const isInt64Bigint = (data: unknown): data is _Int64_bigint => + (typeof data === 'bigint') && isInt64str(data.toString()) + +export const isIntStr = (data: unknown, constraints: { min?: string, max?: string }): data is string => { + const min = constraints.min ?? MIN_INT_64_STR + const max = constraints.max ?? MAX_INT_64_STR + + let hasValidFormat = isString(data) + // check format via RegExp + && /^-?[0-9]*$/.test(data) + // Length checks + && data.length > 0 + && data.length <= Math.max(min.length, max.length) + + let isValidNegativeNumber = isString(data) && data.startsWith("-") && + // leading zeros + (data.length === 2 || data[1] !== "0") && + // if number is negative: greater or equal than min value (Note: this is string comparison!) + (data.length < min.length || data <= min) + let isValidPositiveNumber = isString(data) && !data.startsWith("-") && + // leading zeros + (data.length === 1 || data[0] !== "0") && + // if number is positive: less or equal than max value (Note: this is string comparison!) + (data.length < max.length || data <= max) + + return hasValidFormat && (isValidNegativeNumber || isValidPositiveNumber) +} + export function validate(cond: boolean, errMsg: InvalidDataReason): asserts cond { if (!cond) throw new InvalidData(errMsg) } @@ -89,6 +126,23 @@ export function parseHexStringOfLength(str: unknown, length: L return str } + +export function parseInt64_str(val: unknown, constraints: { min?: string, max?: string}, errMsg: InvalidDataReason): Int64_str { + switch (typeof val) { + case 'string': + validate(isInt64str(val) && isIntStr(val, constraints), errMsg) + return val + case 'number': + validate(isInt64Number(val) && isIntStr(val.toString(), constraints), errMsg) + return val.toString() as Int64_str + case 'bigint': + validate(isInt64Bigint(val) && isIntStr(val.toString(), constraints), errMsg) + return val.toString() as Int64_str + default: + validate(false, errMsg) + } +} + export function parseUint64_str(val: unknown, constraints: { min?: string, max?: string }, errMsg: InvalidDataReason): Uint64_str { switch (typeof val) { case 'string':