import { cborBackend } from "cbor-rpc"; import { MultiAsset } from "./mint"; import { Buffer32 } from "../types"; import { Buffer } from "buffer"; export type Coin = number | bigint; export type HexString = string; export type RawToken = { currencySymbol: string; amount: { tokenName: string; quantity: string }[]; }; export interface AssetMap { [props: HexString]: { [props: HexString]: bigint; }; } export interface AssetMapUtf8 { [props: HexString]: { [props: string]: bigint; }; } export interface NativeAsset { tokenName: HexString; policy: HexString; quantity: bigint; } export interface NativeAssetUtf8 { utf8Name: string; tokenName: HexString; policyId: HexString; quantity: bigint; } /** * ; CDDL Specification for value * * $hash28 = bytes .size 28 * scripthash = $hash28 * policy_id = scripthash * asset_name = bytes .size (0 .. 32) * multiasset = {+ policy_id => {+ asset_name => a0}} * value = coin / [coin, multiasset] **/ export type RawValue = | number | bigint | Map> | (number | bigint | Map>)[]; export function assetMapParse(assetMap: MultiAsset): RawToken[] { const parsedToken: RawToken[] = []; if (assetMap) { Array.from(assetMap).map((arr) => { const currSymbol = arr[0].toString("hex"); const val = arr[1]; const amount = Array.from(val).map(([assetId, amount]) => { const assetIdHex = assetId.toString("hex"); return { tokenName: assetIdHex, quantity: amount.toString(), }; }); const token = { currencySymbol: currSymbol, amount: amount }; parsedToken.push(token); }); } return parsedToken; } export class Value { lovelace: bigint; multiassets?: AssetMap; constructor(lovelace: bigint, multiAssets?: AssetMap) { this.lovelace = lovelace; this.multiassets = multiAssets; } multiAssetsUtf8(): AssetMapUtf8 { const assetMap: Record> = {}; for (let policy in this.multiassets) { let tokens = this.multiassets[policy]; let utf8Tokens: Record = {}; for (let token in tokens) { utf8Tokens[decodeAssetName(token)] = tokens[token]; } assetMap[policy] = utf8Tokens; } return assetMap; } public static sum(values: Value[]): Value { let totalLovelace = BigInt(0); const assetMapSum: Record> = {}; for (const wallet of values) { totalLovelace += wallet.lovelace; for (const policy in wallet.multiassets) { if (!assetMapSum[policy]) { assetMapSum[policy] = {}; } for (const tokenName in wallet.multiassets[policy]) { if (assetMapSum[policy][tokenName] !== undefined) { assetMapSum[policy][tokenName] += wallet.multiassets[policy][tokenName]; } else { assetMapSum[policy][tokenName] = wallet.multiassets[policy][tokenName]; } } } } return new Value(totalLovelace, assetMapSum); } // a= b+ c public add(other: Value): Value { this.lovelace += other.lovelace; for (const policy in other.multiassets) { if (this.multiassets) { if (!this.multiassets[policy]) { this.multiassets[policy] = {}; } for (const tokenName in other.multiassets[policy]) { if (this.multiassets[policy][tokenName] !== undefined) { this.multiassets[policy][tokenName] += other.multiassets[policy][tokenName]; } else { this.multiassets[policy][tokenName] = other.multiassets[policy][tokenName]; } } } } return this; } // a += b public selfAdd(other: Value): void { this.add(other); } public subtract(other: Value): Value { this.lovelace -= other.lovelace; for (const policy in other.multiassets) { if (this.multiassets) { if (this.multiassets[policy]) { for (const tokenName in other.multiassets[policy]) { if (this.multiassets[policy][tokenName] !== undefined) { this.multiassets[policy][tokenName] -= other.multiassets[policy][tokenName]; if (this.multiassets[policy][tokenName] === BigInt(0)) { delete this.multiassets[policy][tokenName]; } } } if (Object.keys(this.multiassets[policy]).length === 0) { delete this.multiassets[policy]; } } } } return this; } public selfSubtract(other: Value): void { this.subtract(other); } public static fromBytes(rawBytes: Buffer): Value { let decodedBytes = cborBackend.decode(rawBytes); return Value.fromCborObject(decodedBytes); } public static fromCborObject(obj: RawValue): Value { let lovelace = BigInt(0); const toBigInt = (val: number | bigint | string): bigint => { if (typeof val === "number" || typeof val === "string") { return BigInt(val); } else { return val; } }; const assets: AssetMap = {}; if (typeof obj === "number" || typeof obj === "bigint") { lovelace = toBigInt(obj); } else if (Array.isArray(obj)) { lovelace = toBigInt(obj[0] as number | bigint); const parsedAssetMap = assetMapParse(obj[1] as Map>); parsedAssetMap.forEach((parsedAsset) => { const policy = parsedAsset.currencySymbol; if (!assets[policy]) { assets[policy] = {}; } parsedAsset.amount.forEach((amount) => { const tokenName = amount.tokenName; const quantity = toBigInt(amount.quantity); assets[policy][tokenName] = quantity; }); }); } return new Value(lovelace, assets); } // compare(b2: WalletBalance){ // } public static fromString(_txt: string) { const txt = _txt.trim(); const values = txt.split("+"); let lovelace = BigInt(0); let assets: AssetMap = {}; const checkValidTokenName = (tokenName: string) => { const hexPattern = /^(0x)?([0-9a-fA-F]{2})+$/i; if (hexPattern.test(tokenName)) { return Buffer.from(tokenName.replace(/^0x/, ""), "hex"); } else { return Buffer.from(tokenName, "utf-8"); } }; try { values.forEach((val) => { val = val.trim(); if (val.length < 1) { return; } const asset = splitValueAndUnit(val); const policyLower = asset.policy.toLowerCase(); if (asset.token === undefined) { // this is ada. if (!isAdaUnit(policyLower)) { throw new Error("Invalid Value component : " + val); } if (policyLower.startsWith("a")) { lovelace += strToBigInt(asset.amount, 6); } else { lovelace += strToBigInt(asset.amount, 0); } return; } if (asset.policy.length !== 56 || !/^[a-fA-F0-9]+$/.test(asset.policy)) { throw new Error("Invalid policy : " + val); } let tokenName: string | Buffer = checkValidTokenName(asset.token); if (!tokenName) { throw new Error("Invalid Value component : " + val); } tokenName = tokenName.toString("utf-8"); if (!assets[asset.policy]) { assets[asset.policy] = {}; } const currentPolicy = assets[asset.policy]; if (currentPolicy[tokenName]) { currentPolicy[tokenName] += strToBigInt(asset.amount, 0); } else { currentPolicy[tokenName] = strToBigInt(asset.amount, 0); } }); return new Value(lovelace, assets); } catch (e1: any) { let result; if (values.length == 1 && /^[a-fA-F0-9]+$/.test(txt)) { try { result = Value.fromCborObject(cborBackend.decode(Buffer.from(txt, "hex"))); } catch (e: any) {} if (result) { return result; } } throw e1; } } private compareAssetMaps(map1?: AssetMap, map2?: AssetMap): number { let keys1: HexString[] = []; let keys2: HexString[] = []; keys1 = Object.keys(map1 ? map1 : {}); keys2 = Object.keys(map2 ? map2 : {}); // Compare top-level keys' lengths if (keys1.length !== keys2.length) { return keys1.length - keys2.length; } // Compare nested maps for (const key of keys1) { if (!keys2.includes(key)) { return -1; // If a key in map1 doesn't exist in map2, map1 < map2 } const innerMap1 = map1 ? map1[key] : {}; const innerMap2 = map2 ? map2[key] : {}; const innerKeys1 = Object.keys(innerMap1); const innerKeys2 = Object.keys(innerMap2); // Compare inner keys' lengths if (innerKeys1.length !== innerKeys2.length) { return innerKeys1.length - innerKeys2.length; } // Compare values within inner keys for (const innerKey of innerKeys1) { if (!innerKeys2.includes(innerKey)) { return -1; // If an innerKey in map1 doesn't exist in map2 } const value1 = innerMap1[innerKey]; const value2 = innerMap2[innerKey]; if (value1 !== value2) { return value1 > value2 ? 1 : -1; } } } return 0; // Maps are equal } greaterThan(other: Value): boolean { // Compare lovelace if (this.lovelace > other.lovelace) { return true; } if (this.lovelace < other.lovelace) { return false; } // Compare asset maps return this.compareAssetMaps(this.multiassets, other.multiassets) > 0; } lessThan(other: Value): boolean { // Compare lovelace if (this.lovelace < other.lovelace) { return true; } if (this.lovelace > other.lovelace) { return false; } // Compare asset maps return this.compareAssetMaps(this.multiassets, other.multiassets) < 0; } equals(other: Value): boolean { // Compare lovelace if (this.lovelace !== other.lovelace) { return false; } // Compare asset maps return this.compareAssetMaps(this.multiassets, other.multiassets) === 0; } greaterThanOrEqualsTo(other: Value): boolean { return this.greaterThan(other) || this.equals(other); } lessThanOrEqualsTo(other: Value): boolean { return this.lessThan(other) || this.equals(other); } toCborObject(): bigint | [bigint, Map>] { const lovelace = this.lovelace; const bufferMap = convertAssetMapToBufferMap(this.multiassets as AssetMap); if (bufferMap.size == 0) { return lovelace; } else { return [lovelace, bufferMap]; } } toBytes(): Buffer { return cborBackend.encode(this.toCborObject()); } // // static fromAny{ // } // multiAssetList(): NativeAsset[] { // const assetList: NativeAsset[] = []; // for (let policy in this.multiassets) { // const tokens = this.multiassets[policy]; // for (let token in tokens) { // console.log(policy, token, tokens[token]); // assetList.push({ // tokenName: token, // policy: policy, // quantity: tokens[token], // }); // } // } // return assetList; // } // multiAssetUtf8List(): NativeAssetUtf8[] { // // @ts-ignore // const list: NativeAssetUtf8[] = this.multiAssetList(); // list.forEach((v) => (v.utf8Name = decodeAssetName(v.tokenName))); // return list; // } // static zero(): WalletBalance { // return new WalletBalance(BigInt(0), {}); // } } function decodeAssetName(asset: string): string { try { return Buffer.from(asset, "hex").toString("utf-8"); } catch (e) { return "0x" + asset; } } function convertAssetMapToBufferMap(assetMap: AssetMap): Map> { const result = new Map>(); for (const policyId in assetMap) { const policyBuffer = Buffer.from(policyId, "hex"); const assets = assetMap[policyId]; const assetMapInner = new Map(); for (const assetName in assets) { const assetNameBuffer = Buffer.from(assetName, "hex"); assetMapInner.set(assetNameBuffer, assets[assetName]); } result.set(policyBuffer, assetMapInner); } return result; } function splitValueAndUnit(str: string) { // Use regex to extract the numeric value (with optional decimal) at the beginning and the rest as the unit const match = str.match(/^(\d+(\.\d+)?)(.*)$/); let unit: string | undefined; let amount: string = "1"; if (!/\s/.test(str)) { // either ada or asset with 1 quantity. if (str.length > 32) { // a token unit = str; } else if (match) { amount = match[1]; unit = match[3]; } } else if (match) { amount = match[1]; // numeric part unit = match[3]; // everything else } if (unit === undefined) { throw new Error("Cannot parse value component : " + str); } const asset = unit.split("."); return { amount, token: asset[1], policy: (asset[0] || "l").trim() }; } const adaSet = new Set(); adaSet.add("a"); adaSet.add("ada"); adaSet.add("l"); adaSet.add("lovelace"); adaSet.add("love"); function isAdaUnit(str: string) { return adaSet.has(str); } function strToBigInt(str: string, shift: number) { // Split the string into integer and decimal parts const [integerPart, decimalPart = ""] = str.split("."); // Take only the first 'shift' digits from the decimal part const truncatedDecimal = decimalPart.slice(0, shift); // Combine the integer part and the truncated decimal part const shiftedValue = integerPart + truncatedDecimal + "0".repeat(shift - truncatedDecimal.length); // Return the result as BigInt return BigInt(shiftedValue); } export function valuetoObject(value: any): [bigint, Map>] | bigint { const lovelace = BigInt(value.lovelace as bigint | number | string); let assets: Map> = new Map(); for (let policy in value) { const assetMap = value[policy] as Record; const policyBuffer = Buffer.from(policy, "hex"); for (let tokenName in assetMap) { const tokenNameBuffer = Buffer.from(tokenName, "hex"); if (assets.has(policyBuffer)) { const policyAssets = assets.get(policyBuffer); if (policyAssets) { policyAssets.set(tokenNameBuffer, BigInt(assetMap[tokenName])); } } else { let quantityMap = new Map(); quantityMap.set(tokenNameBuffer, BigInt(assetMap[tokenName])); assets.set(policyBuffer, quantityMap); } } } if (assets.size > 0) { return [lovelace, assets]; } else { return lovelace; } } // const lvl = // "3Ada + 2 34e6054e3e74ecf4a9b6eb4e34a865f6b0b5fee85e6a9bfba0d2d025.366b546f6b656e73 + 3 a48a63ca4083b142057f7f62599ab1e7ca73d3ba6db800e25c1e6e83.50726f706f73616c"; // const val = Value.fromBytes( // Buffer.from( // "821B0000012C4D23A264A4581C34E6054E3E74ECF4A9B6EB4E34A865F6B0B5FEE85E6A9BFBA0D2D025A948366B546F6B656E7319157743414243194D8A43444546194DB0434E47451903E8454F574E45520144526F6F6D0449536967546F6B656E731907CE46546F6B656E311B000009184E72A2BB46546F6B656E3218BD581CA48A63CA4083B142057F7F62599AB1E7CA73D3BA6DB800E25C1E6E83A14850726F706F73616C04581CEFF7E215E355546CD0A59F0307638264CB98B88C8DCBBB36CAD29818A14850726F706F73616C03581CF123088056B0F07927AC7D23CC22F9BC03E4B2FDF0F2E2DF605B58CAA14850726F706F73616C04", // "hex" // ) // ); // console.log(val.toCborObject());