From 9d0d87e3ae7743925ad3979a2b6f1cbe6dd107c1 Mon Sep 17 00:00:00 2001 From: yHSJ Date: Wed, 1 Nov 2023 11:37:22 -0400 Subject: [PATCH 01/54] feat: free references in Data.from --- src/plutus/data.ts | 418 ++++++++++++++++++++---------------------- src/utils/freeable.ts | 13 ++ 2 files changed, 216 insertions(+), 215 deletions(-) create mode 100644 src/utils/freeable.ts diff --git a/src/plutus/data.ts b/src/plutus/data.ts index 2468c725..994a4048 100644 --- a/src/plutus/data.ts +++ b/src/plutus/data.ts @@ -10,6 +10,7 @@ import { import { C } from "../core/mod.ts"; import { Datum, Exact, Json, Redeemer } from "../types/mod.ts"; import { fromHex, fromText, toHex } from "../utils/utils.ts"; +import { Freeable, Freeables } from "../utils/freeable.ts"; export class Constr { index: number; @@ -38,14 +39,12 @@ export type Data = export const Data = { // Types // Note: Recursive types are not supported (yet) - Integer: function ( - options?: { - minimum?: number; - maximum?: number; - exclusiveMinimum?: number; - exclusiveMaximum?: number; - }, - ) { + Integer: function (options?: { + minimum?: number; + maximum?: number; + exclusiveMinimum?: number; + exclusiveMaximum?: number; + }) { const integer = Type.Unsafe({ dataType: "integer" }); if (options) { Object.entries(options).forEach(([key, value]) => { @@ -54,9 +53,11 @@ export const Data = { } return integer; }, - Bytes: function ( - options?: { minLength?: number; maxLength?: number; enum?: string[] }, - ) { + Bytes: function (options?: { + minLength?: number; + maxLength?: number; + enum?: string[]; + }) { const bytes = Type.Unsafe({ dataType: "bytes" }); if (options) { Object.entries(options).forEach(([key, value]) => { @@ -88,7 +89,7 @@ export const Data = { }, Array: function ( items: T, - options?: { minItems?: number; maxItems?: number; uniqueItems?: boolean }, + options?: { minItems?: number; maxItems?: number; uniqueItems?: boolean } ) { const array = Type.Array(items); replaceProperties(array, { dataType: "list", items }); @@ -102,7 +103,7 @@ export const Data = { Map: function ( keys: T, values: U, - options?: { minItems?: number; maxItems?: number }, + options?: { minItems?: number; maxItems?: number } ) { const map = Type.Unsafe, Data.Static>>({ dataType: "map", @@ -122,55 +123,55 @@ export const Data = { */ Object: function ( properties: T, - options?: { hasConstr?: boolean }, + options?: { hasConstr?: boolean } ) { const object = Type.Object(properties); replaceProperties(object, { - anyOf: [{ - dataType: "constructor", - index: 0, // Will be replaced when using Data.Enum - fields: Object.entries(properties).map(([title, p]) => ({ - ...p, - title, - })), - }], + anyOf: [ + { + dataType: "constructor", + index: 0, // Will be replaced when using Data.Enum + fields: Object.entries(properties).map(([title, p]) => ({ + ...p, + title, + })), + }, + ], }); - object.anyOf[0].hasConstr = typeof options?.hasConstr === "undefined" || - options.hasConstr; + object.anyOf[0].hasConstr = + typeof options?.hasConstr === "undefined" || options.hasConstr; return object; }, Enum: function (items: T[]) { const union = Type.Union(items); - replaceProperties( - union, - { - anyOf: items.map((item, index) => - item.anyOf[0].fields.length === 0 - ? ({ + replaceProperties(union, { + anyOf: items.map((item, index) => + item.anyOf[0].fields.length === 0 + ? { ...item.anyOf[0], index, - }) - : ({ + } + : { dataType: "constructor", title: (() => { const title = item.anyOf[0].fields[0].title; if ( (title as string).charAt(0) !== - (title as string).charAt(0).toUpperCase() + (title as string).charAt(0).toUpperCase() ) { throw new Error( - `Enum '${title}' needs to start with an uppercase letter.`, + `Enum '${title}' needs to start with an uppercase letter.` ); } return item.anyOf[0].fields[0].title; })(), index, - fields: item.anyOf[0].fields[0].items || + fields: + item.anyOf[0].fields[0].items || item.anyOf[0].fields[0].anyOf[0].fields, - }) - ), - }, - ); + } + ), + }); return union; }, /** @@ -179,7 +180,7 @@ export const Data = { */ Tuple: function ( items: [...T], - options?: { hasConstr?: boolean }, + options?: { hasConstr?: boolean } ) { const tuple = Type.Tuple(items); replaceProperties(tuple, { @@ -198,17 +199,19 @@ export const Data = { (title as string).charAt(0) !== (title as string).charAt(0).toUpperCase() ) { throw new Error( - `Enum '${title}' needs to start with an uppercase letter.`, + `Enum '${title}' needs to start with an uppercase letter.` ); } const literal = Type.Literal(title); replaceProperties(literal, { - anyOf: [{ - dataType: "constructor", - title, - index: 0, // Will be replaced in Data.Enum - fields: [], - }], + anyOf: [ + { + dataType: "constructor", + title, + index: 0, // Will be replaced in Data.Enum + fields: [], + }, + ], }); return literal; }, @@ -220,9 +223,7 @@ export const Data = { description: "An optional value.", dataType: "constructor", index: 0, - fields: [ - item, - ], + fields: [item], }, { title: "None", @@ -265,9 +266,7 @@ export const Data = { function to(data: Exact, type?: T): Datum | Redeemer { function serialize(data: Data): C.PlutusData { try { - if ( - typeof data === "bigint" - ) { + if (typeof data === "bigint") { return C.PlutusData.new_integer(C.BigInt.from_str(data.toString())); } else if (typeof data === "string") { return C.PlutusData.new_bytes(fromHex(data)); @@ -280,8 +279,8 @@ function to(data: Exact, type?: T): Datum | Redeemer { return C.PlutusData.new_constr_plutus_data( C.ConstrPlutusData.new( C.BigNum.from_str(index.toString()), - plutusList, - ), + plutusList + ) ); } else if (data instanceof Array) { const plutusList = C.PlutusList.new(); @@ -303,7 +302,7 @@ function to(data: Exact, type?: T): Datum | Redeemer { throw new Error("Could not serialize the data: " + error); } } - const d = type ? castTo(data, type) : data as Data; + const d = type ? castTo(data, type) : (data as Data); return toHex(serialize(d).to_bytes()) as Datum | Redeemer; } @@ -313,39 +312,63 @@ function to(data: Exact, type?: T): Datum | Redeemer { */ function from(raw: Datum | Redeemer, type?: T): T { function deserialize(data: C.PlutusData): Data { - if (data.kind() === 0) { - const constr = data.as_constr_plutus_data()!; - const l = constr.data(); - const desL = []; - for (let i = 0; i < l.len(); i++) { - desL.push(deserialize(l.get(i))); - } - return new Constr(parseInt(constr.alternative().to_str()), desL); - } else if (data.kind() === 1) { - const m = data.as_map()!; - const desM: Map = new Map(); - const keys = m.keys(); - for (let i = 0; i < keys.len(); i++) { - desM.set(deserialize(keys.get(i)), deserialize(m.get(keys.get(i))!)); - } - return desM; - } else if (data.kind() === 2) { - const l = data.as_list()!; - const desL = []; - for (let i = 0; i < l.len(); i++) { - desL.push(deserialize(l.get(i))); + const bucket: Freeable[] = []; + try { + if (data.kind() === 0) { + const constr = data.as_constr_plutus_data()!; + bucket.push(constr); + const l = constr.data(); + bucket.push(l); + const desL = []; + for (let i = 0; i < l.len(); i++) { + const des = l.get(i); + bucket.push(des); + desL.push(deserialize(des)); + } + const alternativeConstr = constr.alternative(); + bucket.push(alternativeConstr); + return new Constr(parseInt(alternativeConstr.to_str()), desL); + } else if (data.kind() === 1) { + const m = data.as_map()!; + bucket.push(m); + const desM: Map = new Map(); + const keys = m.keys(); + bucket.push(keys); + for (let i = 0; i < keys.len(); i++) { + const key = keys.get(i); + bucket.push(key); + const value = m.get(key)!; + bucket.push(value); + desM.set(deserialize(key), deserialize(value)); + } + return desM; + } else if (data.kind() === 2) { + const l = data.as_list()!; + bucket.push(l); + const desL = []; + for (let i = 0; i < l.len(); i++) { + const elem = l.get(i); + bucket.push(elem); + desL.push(deserialize(elem)); + } + return desL; + } else if (data.kind() === 3) { + const i = data.as_integer()!; + bucket.push(i); + return BigInt(i.to_str()); + } else if (data.kind() === 4) { + return toHex(data.as_bytes()!); } - return desL; - } else if (data.kind() === 3) { - return BigInt(data.as_integer()!.to_str()); - } else if (data.kind() === 4) { - return toHex(data.as_bytes()!); + throw new Error("Unsupported type"); + } finally { + Freeables.free(...bucket); } - throw new Error("Unsupported type"); } - const data = deserialize(C.PlutusData.from_bytes(fromHex(raw))); + const plutusData = C.PlutusData.from_bytes(fromHex(raw)); + const data = deserialize(plutusData); + plutusData.free(); - return type ? castFrom(data, type) : data as T; + return type ? castFrom(data, type) : (data as T); } /** @@ -386,15 +409,14 @@ function toJson(plutusData: Data): Json { !isNaN(parseInt(data)) && data.slice(-1) === "n") ) { - const bigint = typeof data === "string" - ? BigInt(data.slice(0, -1)) - : data; + const bigint = + typeof data === "string" ? BigInt(data.slice(0, -1)) : data; return parseInt(bigint.toString()); } if (typeof data === "string") { try { return new TextDecoder(undefined, { fatal: true }).decode( - fromHex(data), + fromHex(data) ); } catch (_) { return "0x" + toHex(fromHex(data)); @@ -410,7 +432,7 @@ function toJson(plutusData: Data): Json { typeof convertedKey !== "number" ) { throw new Error( - "Unsupported type (Note: Only bytes or integers can be keys of a JSON object)", + "Unsupported type (Note: Only bytes or integers can be keys of a JSON object)" ); } tempJson[convertedKey] = fromData(value); @@ -418,7 +440,7 @@ function toJson(plutusData: Data): Json { return tempJson; } throw new Error( - "Unsupported type (Note: Constructor cannot be converted to JSON)", + "Unsupported type (Note: Constructor cannot be converted to JSON)" ); } return fromData(plutusData); @@ -447,59 +469,52 @@ function castFrom(data: Data, type: T): T { case "constructor": { if (isVoid(shape)) { if ( - !(data instanceof Constr) || data.index !== 0 || + !(data instanceof Constr) || + data.index !== 0 || data.fields.length !== 0 ) { throw new Error("Could not type cast to void."); } return undefined as T; } else if ( - data instanceof Constr && data.index === shape.index && + data instanceof Constr && + data.index === shape.index && (shape.hasConstr || shape.hasConstr === undefined) ) { const fields: Record = {}; if (shape.fields.length !== data.fields.length) { throw new Error( - "Could not type cast to object. Fields do not match.", + "Could not type cast to object. Fields do not match." ); } - shape.fields.forEach( - (field: Json, fieldIndex: number) => { - const title = field.title || "wrapper"; - if ((/[A-Z]/.test(title[0]))) { - throw new Error( - "Could not type cast to object. Object properties need to start with a lowercase letter.", - ); - } - fields[title] = castFrom( - data.fields[fieldIndex], - field, + shape.fields.forEach((field: Json, fieldIndex: number) => { + const title = field.title || "wrapper"; + if (/[A-Z]/.test(title[0])) { + throw new Error( + "Could not type cast to object. Object properties need to start with a lowercase letter." ); - }, - ); + } + fields[title] = castFrom(data.fields[fieldIndex], field); + }); return fields as T; } else if ( - data instanceof Array && !shape.hasConstr && + data instanceof Array && + !shape.hasConstr && shape.hasConstr !== undefined ) { const fields: Record = {}; if (shape.fields.length !== data.length) { throw new Error("Could not ype cast to object. Fields do not match."); } - shape.fields.forEach( - (field: Json, fieldIndex: number) => { - const title = field.title || "wrapper"; - if ((/[A-Z]/.test(title[0]))) { - throw new Error( - "Could not type cast to object. Object properties need to start with a lowercase letter.", - ); - } - fields[title] = castFrom( - data[fieldIndex], - field, + shape.fields.forEach((field: Json, fieldIndex: number) => { + const title = field.title || "wrapper"; + if (/[A-Z]/.test(title[0])) { + throw new Error( + "Could not type cast to object. Object properties need to start with a lowercase letter." ); - }, - ); + } + fields[title] = castFrom(data[fieldIndex], field); + }); return fields as T; } throw new Error("Could not type cast to object."); @@ -514,8 +529,8 @@ function castFrom(data: Data, type: T): T { throw new Error("Could not type cast to enum."); } - const enumShape = shape.anyOf.find((entry: Json) => - entry.index === data.index + const enumShape = shape.anyOf.find( + (entry: Json) => entry.index === data.index ); if (!enumShape || enumShape.fields.length !== data.fields.length) { throw new Error("Could not type cast to enum."); @@ -534,17 +549,13 @@ function castFrom(data: Data, type: T): T { } else if (isNullable(shape)) { switch (data.index) { case 0: { - if ( - data.fields.length !== 1 - ) { + if (data.fields.length !== 1) { throw new Error("Could not type cast to nullable object."); } return castFrom(data.fields[0], shape.anyOf[0].fields[0]); } case 1: { - if ( - data.fields.length !== 0 - ) { + if (data.fields.length !== 0) { throw new Error("Could not type cast to nullable object."); } return null as T; @@ -555,16 +566,14 @@ function castFrom(data: Data, type: T): T { switch (enumShape.dataType) { case "constructor": { if (enumShape.fields.length === 0) { - if ( - /[A-Z]/.test(enumShape.title[0]) - ) { + if (/[A-Z]/.test(enumShape.title[0])) { return enumShape.title as T; } throw new Error("Could not type cast to enum."); } else { - if (!(/[A-Z]/.test(enumShape.title))) { + if (!/[A-Z]/.test(enumShape.title)) { throw new Error( - "Could not type cast to enum. Enums need to start with an uppercase letter.", + "Could not type cast to enum. Enums need to start with an uppercase letter." ); } @@ -574,14 +583,15 @@ function castFrom(data: Data, type: T): T { // check if named args const args = enumShape.fields[0].title - ? Object.fromEntries(enumShape.fields.map(( - field: Json, - index: number, - ) => [field.title, castFrom(data.fields[index], field)])) - : enumShape.fields.map(( - field: Json, - index: number, - ) => castFrom(data.fields[index], field)); + ? Object.fromEntries( + enumShape.fields.map((field: Json, index: number) => [ + field.title, + castFrom(data.fields[index], field), + ]) + ) + : enumShape.fields.map((field: Json, index: number) => + castFrom(data.fields[index], field) + ); return { [enumShape.title]: args, @@ -594,11 +604,7 @@ function castFrom(data: Data, type: T): T { case "list": { if (shape.items instanceof Array) { // tuple - if ( - data instanceof Constr && - data.index === 0 && - shape.hasConstr - ) { + if (data instanceof Constr && data.index === 0 && shape.hasConstr) { return data.fields.map((field, index) => castFrom(field, shape.items[index]) ) as T; @@ -625,10 +631,7 @@ function castFrom(data: Data, type: T): T { } mapConstraints(data, shape); const map = new Map(); - for ( - const [key, value] of (data) - .entries() - ) { + for (const [key, value] of data.entries()) { map.set(castFrom(key, shape.keys), castFrom(value, shape.values)); } return map as T; @@ -667,7 +670,8 @@ function castTo(struct: Exact, type: T): Data { } return new Constr(0, []); } else if ( - typeof struct !== "object" || struct === null || + typeof struct !== "object" || + struct === null || shape.fields.length !== Object.keys(struct).length ) { throw new Error("Could not type cast to constructor."); @@ -675,10 +679,10 @@ function castTo(struct: Exact, type: T): Data { const fields = shape.fields.map((field: Json) => castTo( (struct as Record)[field.title || "wrapper"], - field, + field ) ); - return (shape.hasConstr || shape.hasConstr === undefined) + return shape.hasConstr || shape.hasConstr === undefined ? new Constr(shape.index, fields) : fields; } @@ -690,9 +694,7 @@ function castTo(struct: Exact, type: T): Data { if (isBoolean(shape)) { if (typeof struct !== "boolean") { - throw new Error( - "Could not type cast to boolean.", - ); + throw new Error("Could not type cast to boolean."); } return new Constr(struct ? 1 : 0, []); } else if (isNullable(shape)) { @@ -702,24 +704,21 @@ function castTo(struct: Exact, type: T): Data { if (fields.length !== 1) { throw new Error("Could not type cast to nullable object."); } - return new Constr(0, [ - castTo(struct, fields[0]), - ]); + return new Constr(0, [castTo(struct, fields[0])]); } } switch (typeof struct) { case "string": { - if (!(/[A-Z]/.test(struct[0]))) { + if (!/[A-Z]/.test(struct[0])) { throw new Error( - "Could not type cast to enum. Enum needs to start with an uppercase letter.", + "Could not type cast to enum. Enum needs to start with an uppercase letter." ); } - const enumIndex = (shape as TEnum).anyOf.findIndex(( - s: TLiteral, - ) => - s.dataType === "constructor" && - s.fields.length === 0 && - s.title === struct + const enumIndex = (shape as TEnum).anyOf.findIndex( + (s: TLiteral) => + s.dataType === "constructor" && + s.fields.length === 0 && + s.title === struct ); if (enumIndex === -1) throw new Error("Could not type cast to enum."); return new Constr(enumIndex, []); @@ -728,14 +727,13 @@ function castTo(struct: Exact, type: T): Data { if (struct === null) throw new Error("Could not type cast to enum."); const structTitle = Object.keys(struct)[0]; - if (!(/[A-Z]/.test(structTitle))) { + if (!/[A-Z]/.test(structTitle)) { throw new Error( - "Could not type cast to enum. Enum needs to start with an uppercase letter.", + "Could not type cast to enum. Enum needs to start with an uppercase letter." ); } - const enumEntry = shape.anyOf.find((s: Json) => - s.dataType === "constructor" && - s.title === structTitle + const enumEntry = shape.anyOf.find( + (s: Json) => s.dataType === "constructor" && s.title === structTitle ); if (!enumEntry) throw new Error("Could not type cast to enum."); @@ -747,16 +745,14 @@ function castTo(struct: Exact, type: T): Data { // check if named args args instanceof Array ? args.map((item, index) => - castTo(item, enumEntry.fields[index]) - ) - : enumEntry.fields.map( - (entry: Json) => { - const [_, item]: [string, Json] = Object.entries(args).find(( - [title], - ) => title === entry.title)!; + castTo(item, enumEntry.fields[index]) + ) + : enumEntry.fields.map((entry: Json) => { + const [_, item]: [string, Json] = Object.entries(args).find( + ([title]) => title === entry.title + )!; return castTo(item, entry); - }, - ), + }) ); } } @@ -786,10 +782,7 @@ function castTo(struct: Exact, type: T): Data { mapConstraints(struct, shape); const map = new Map(); - for ( - const [key, value] of (struct) - .entries() - ) { + for (const [key, value] of struct.entries()) { map.set(castTo(key, shape.keys), castTo(value, shape.values)); } return map; @@ -804,79 +797,71 @@ function castTo(struct: Exact, type: T): Data { function integerConstraints(integer: bigint, shape: TSchema) { if (shape.minimum && integer < BigInt(shape.minimum)) { throw new Error( - `Integer ${integer} is below the minimum ${shape.minimum}.`, + `Integer ${integer} is below the minimum ${shape.minimum}.` ); } if (shape.maximum && integer > BigInt(shape.maximum)) { throw new Error( - `Integer ${integer} is above the maxiumum ${shape.maximum}.`, + `Integer ${integer} is above the maxiumum ${shape.maximum}.` ); } if (shape.exclusiveMinimum && integer <= BigInt(shape.exclusiveMinimum)) { throw new Error( - `Integer ${integer} is below the exclusive minimum ${shape.exclusiveMinimum}.`, + `Integer ${integer} is below the exclusive minimum ${shape.exclusiveMinimum}.` ); } if (shape.exclusiveMaximum && integer >= BigInt(shape.exclusiveMaximum)) { throw new Error( - `Integer ${integer} is above the exclusive maximum ${shape.exclusiveMaximum}.`, + `Integer ${integer} is above the exclusive maximum ${shape.exclusiveMaximum}.` ); } } function bytesConstraints(bytes: string, shape: TSchema) { - if ( - shape.enum && !shape.enum.some((keyword: string) => keyword === bytes) - ) throw new Error(`None of the keywords match with '${bytes}'.`); + if (shape.enum && !shape.enum.some((keyword: string) => keyword === bytes)) + throw new Error(`None of the keywords match with '${bytes}'.`); if (shape.minLength && bytes.length / 2 < shape.minLength) { throw new Error( - `Bytes need to have a length of at least ${shape.minLength} bytes.`, + `Bytes need to have a length of at least ${shape.minLength} bytes.` ); } if (shape.maxLength && bytes.length / 2 > shape.maxLength) { throw new Error( - `Bytes can have a length of at most ${shape.minLength} bytes.`, + `Bytes can have a length of at most ${shape.minLength} bytes.` ); } } function listConstraints(list: Array, shape: TSchema) { if (shape.minItems && list.length < shape.minItems) { - throw new Error( - `Array needs to contain at least ${shape.minItems} items.`, - ); + throw new Error(`Array needs to contain at least ${shape.minItems} items.`); } if (shape.maxItems && list.length > shape.maxItems) { - throw new Error( - `Array can contain at most ${shape.maxItems} items.`, - ); + throw new Error(`Array can contain at most ${shape.maxItems} items.`); } - if (shape.uniqueItems && (new Set(list)).size !== list.length) { + if (shape.uniqueItems && new Set(list).size !== list.length) { // Note this only works for primitive types like string and bigint. - throw new Error( - "Array constains duplicates.", - ); + throw new Error("Array constains duplicates."); } } function mapConstraints(map: Map, shape: TSchema) { if (shape.minItems && map.size < shape.minItems) { - throw new Error( - `Map needs to contain at least ${shape.minItems} items.`, - ); + throw new Error(`Map needs to contain at least ${shape.minItems} items.`); } if (shape.maxItems && map.size > shape.maxItems) { - throw new Error( - `Map can contain at most ${shape.maxItems} items.`, - ); + throw new Error(`Map can contain at most ${shape.maxItems} items.`); } } function isBoolean(shape: TSchema): boolean { - return shape.anyOf && shape.anyOf[0]?.title === "False" && - shape.anyOf[1]?.title === "True"; + return ( + shape.anyOf && + shape.anyOf[0]?.title === "False" && + shape.anyOf[1]?.title === "True" + ); } function isVoid(shape: TSchema): boolean { @@ -884,8 +869,11 @@ function isVoid(shape: TSchema): boolean { } function isNullable(shape: TSchema): boolean { - return shape.anyOf && shape.anyOf[0]?.title === "Some" && - shape.anyOf[1]?.title === "None"; + return ( + shape.anyOf && + shape.anyOf[0]?.title === "Some" && + shape.anyOf[1]?.title === "None" + ); } function replaceProperties(object: Json, properties: Json) { diff --git a/src/utils/freeable.ts b/src/utils/freeable.ts new file mode 100644 index 00000000..651f5835 --- /dev/null +++ b/src/utils/freeable.ts @@ -0,0 +1,13 @@ +export interface Freeable { + free(): void; +} + +export type FreeableBucket = Array; + +export abstract class Freeables { + static free(...bucket: (Freeable | undefined)[]) { + bucket.forEach((freeable) => { + freeable?.free(); + }); + } +} From ff1f03b6bf79b1699e3b36cd4343818c04ef1f77 Mon Sep 17 00:00:00 2001 From: yHSJ Date: Wed, 1 Nov 2023 12:07:11 -0400 Subject: [PATCH 02/54] fix: formatting --- docs/docs/getting-started/choose-provider.md | 6 +- src/plutus/data.ts | 132 ++++++++++--------- 2 files changed, 70 insertions(+), 68 deletions(-) diff --git a/docs/docs/getting-started/choose-provider.md b/docs/docs/getting-started/choose-provider.md index 57e70046..920750a5 100644 --- a/docs/docs/getting-started/choose-provider.md +++ b/docs/docs/getting-started/choose-provider.md @@ -46,9 +46,9 @@ import { Lucid, Maestro } from "https://deno.land/x/lucid/mod.ts"; const lucid = await Lucid.new( new Maestro({ - network: "Preprod", // For MAINNET: "Mainnet". - apiKey: "", // Get yours by visiting https://docs.gomaestro.org/docs/Getting-started/Sign-up-login. - turboSubmit: false // Read about paid turbo transaction submission feature at https://docs.gomaestro.org/docs/Dapp%20Platform/Turbo%20Transaction. + network: "Preprod", // For MAINNET: "Mainnet". + apiKey: "", // Get yours by visiting https://docs.gomaestro.org/docs/Getting-started/Sign-up-login. + turboSubmit: false, // Read about paid turbo transaction submission feature at https://docs.gomaestro.org/docs/Dapp%20Platform/Turbo%20Transaction. }), "Preprod", // For MAINNET: "Mainnet". ); diff --git a/src/plutus/data.ts b/src/plutus/data.ts index 994a4048..63e028ca 100644 --- a/src/plutus/data.ts +++ b/src/plutus/data.ts @@ -89,7 +89,7 @@ export const Data = { }, Array: function ( items: T, - options?: { minItems?: number; maxItems?: number; uniqueItems?: boolean } + options?: { minItems?: number; maxItems?: number; uniqueItems?: boolean }, ) { const array = Type.Array(items); replaceProperties(array, { dataType: "list", items }); @@ -103,7 +103,7 @@ export const Data = { Map: function ( keys: T, values: U, - options?: { minItems?: number; maxItems?: number } + options?: { minItems?: number; maxItems?: number }, ) { const map = Type.Unsafe, Data.Static>>({ dataType: "map", @@ -123,7 +123,7 @@ export const Data = { */ Object: function ( properties: T, - options?: { hasConstr?: boolean } + options?: { hasConstr?: boolean }, ) { const object = Type.Object(properties); replaceProperties(object, { @@ -138,8 +138,8 @@ export const Data = { }, ], }); - object.anyOf[0].hasConstr = - typeof options?.hasConstr === "undefined" || options.hasConstr; + object.anyOf[0].hasConstr = typeof options?.hasConstr === "undefined" || + options.hasConstr; return object; }, Enum: function (items: T[]) { @@ -148,28 +148,27 @@ export const Data = { anyOf: items.map((item, index) => item.anyOf[0].fields.length === 0 ? { - ...item.anyOf[0], - index, - } + ...item.anyOf[0], + index, + } : { - dataType: "constructor", - title: (() => { - const title = item.anyOf[0].fields[0].title; - if ( - (title as string).charAt(0) !== + dataType: "constructor", + title: (() => { + const title = item.anyOf[0].fields[0].title; + if ( + (title as string).charAt(0) !== (title as string).charAt(0).toUpperCase() - ) { - throw new Error( - `Enum '${title}' needs to start with an uppercase letter.` - ); - } - return item.anyOf[0].fields[0].title; - })(), - index, - fields: - item.anyOf[0].fields[0].items || - item.anyOf[0].fields[0].anyOf[0].fields, - } + ) { + throw new Error( + `Enum '${title}' needs to start with an uppercase letter.`, + ); + } + return item.anyOf[0].fields[0].title; + })(), + index, + fields: item.anyOf[0].fields[0].items || + item.anyOf[0].fields[0].anyOf[0].fields, + } ), }); return union; @@ -180,7 +179,7 @@ export const Data = { */ Tuple: function ( items: [...T], - options?: { hasConstr?: boolean } + options?: { hasConstr?: boolean }, ) { const tuple = Type.Tuple(items); replaceProperties(tuple, { @@ -199,7 +198,7 @@ export const Data = { (title as string).charAt(0) !== (title as string).charAt(0).toUpperCase() ) { throw new Error( - `Enum '${title}' needs to start with an uppercase letter.` + `Enum '${title}' needs to start with an uppercase letter.`, ); } const literal = Type.Literal(title); @@ -279,8 +278,8 @@ function to(data: Exact, type?: T): Datum | Redeemer { return C.PlutusData.new_constr_plutus_data( C.ConstrPlutusData.new( C.BigNum.from_str(index.toString()), - plutusList - ) + plutusList, + ), ); } else if (data instanceof Array) { const plutusList = C.PlutusList.new(); @@ -409,14 +408,15 @@ function toJson(plutusData: Data): Json { !isNaN(parseInt(data)) && data.slice(-1) === "n") ) { - const bigint = - typeof data === "string" ? BigInt(data.slice(0, -1)) : data; + const bigint = typeof data === "string" + ? BigInt(data.slice(0, -1)) + : data; return parseInt(bigint.toString()); } if (typeof data === "string") { try { return new TextDecoder(undefined, { fatal: true }).decode( - fromHex(data) + fromHex(data), ); } catch (_) { return "0x" + toHex(fromHex(data)); @@ -432,7 +432,7 @@ function toJson(plutusData: Data): Json { typeof convertedKey !== "number" ) { throw new Error( - "Unsupported type (Note: Only bytes or integers can be keys of a JSON object)" + "Unsupported type (Note: Only bytes or integers can be keys of a JSON object)", ); } tempJson[convertedKey] = fromData(value); @@ -440,7 +440,7 @@ function toJson(plutusData: Data): Json { return tempJson; } throw new Error( - "Unsupported type (Note: Constructor cannot be converted to JSON)" + "Unsupported type (Note: Constructor cannot be converted to JSON)", ); } return fromData(plutusData); @@ -484,14 +484,14 @@ function castFrom(data: Data, type: T): T { const fields: Record = {}; if (shape.fields.length !== data.fields.length) { throw new Error( - "Could not type cast to object. Fields do not match." + "Could not type cast to object. Fields do not match.", ); } shape.fields.forEach((field: Json, fieldIndex: number) => { const title = field.title || "wrapper"; if (/[A-Z]/.test(title[0])) { throw new Error( - "Could not type cast to object. Object properties need to start with a lowercase letter." + "Could not type cast to object. Object properties need to start with a lowercase letter.", ); } fields[title] = castFrom(data.fields[fieldIndex], field); @@ -510,7 +510,7 @@ function castFrom(data: Data, type: T): T { const title = field.title || "wrapper"; if (/[A-Z]/.test(title[0])) { throw new Error( - "Could not type cast to object. Object properties need to start with a lowercase letter." + "Could not type cast to object. Object properties need to start with a lowercase letter.", ); } fields[title] = castFrom(data[fieldIndex], field); @@ -530,7 +530,7 @@ function castFrom(data: Data, type: T): T { } const enumShape = shape.anyOf.find( - (entry: Json) => entry.index === data.index + (entry: Json) => entry.index === data.index, ); if (!enumShape || enumShape.fields.length !== data.fields.length) { throw new Error("Could not type cast to enum."); @@ -573,7 +573,7 @@ function castFrom(data: Data, type: T): T { } else { if (!/[A-Z]/.test(enumShape.title)) { throw new Error( - "Could not type cast to enum. Enums need to start with an uppercase letter." + "Could not type cast to enum. Enums need to start with an uppercase letter.", ); } @@ -584,14 +584,14 @@ function castFrom(data: Data, type: T): T { // check if named args const args = enumShape.fields[0].title ? Object.fromEntries( - enumShape.fields.map((field: Json, index: number) => [ - field.title, - castFrom(data.fields[index], field), - ]) - ) + enumShape.fields.map((field: Json, index: number) => [ + field.title, + castFrom(data.fields[index], field), + ]), + ) : enumShape.fields.map((field: Json, index: number) => - castFrom(data.fields[index], field) - ); + castFrom(data.fields[index], field) + ); return { [enumShape.title]: args, @@ -679,7 +679,7 @@ function castTo(struct: Exact, type: T): Data { const fields = shape.fields.map((field: Json) => castTo( (struct as Record)[field.title || "wrapper"], - field + field, ) ); return shape.hasConstr || shape.hasConstr === undefined @@ -711,14 +711,14 @@ function castTo(struct: Exact, type: T): Data { case "string": { if (!/[A-Z]/.test(struct[0])) { throw new Error( - "Could not type cast to enum. Enum needs to start with an uppercase letter." + "Could not type cast to enum. Enum needs to start with an uppercase letter.", ); } const enumIndex = (shape as TEnum).anyOf.findIndex( (s: TLiteral) => s.dataType === "constructor" && s.fields.length === 0 && - s.title === struct + s.title === struct, ); if (enumIndex === -1) throw new Error("Could not type cast to enum."); return new Constr(enumIndex, []); @@ -729,11 +729,12 @@ function castTo(struct: Exact, type: T): Data { if (!/[A-Z]/.test(structTitle)) { throw new Error( - "Could not type cast to enum. Enum needs to start with an uppercase letter." + "Could not type cast to enum. Enum needs to start with an uppercase letter.", ); } const enumEntry = shape.anyOf.find( - (s: Json) => s.dataType === "constructor" && s.title === structTitle + (s: Json) => + s.dataType === "constructor" && s.title === structTitle, ); if (!enumEntry) throw new Error("Could not type cast to enum."); @@ -745,14 +746,14 @@ function castTo(struct: Exact, type: T): Data { // check if named args args instanceof Array ? args.map((item, index) => - castTo(item, enumEntry.fields[index]) - ) + castTo(item, enumEntry.fields[index]) + ) : enumEntry.fields.map((entry: Json) => { - const [_, item]: [string, Json] = Object.entries(args).find( - ([title]) => title === entry.title - )!; - return castTo(item, entry); - }) + const [_, item]: [string, Json] = Object.entries(args).find( + ([title]) => title === entry.title, + )!; + return castTo(item, entry); + }), ); } } @@ -797,38 +798,39 @@ function castTo(struct: Exact, type: T): Data { function integerConstraints(integer: bigint, shape: TSchema) { if (shape.minimum && integer < BigInt(shape.minimum)) { throw new Error( - `Integer ${integer} is below the minimum ${shape.minimum}.` + `Integer ${integer} is below the minimum ${shape.minimum}.`, ); } if (shape.maximum && integer > BigInt(shape.maximum)) { throw new Error( - `Integer ${integer} is above the maxiumum ${shape.maximum}.` + `Integer ${integer} is above the maxiumum ${shape.maximum}.`, ); } if (shape.exclusiveMinimum && integer <= BigInt(shape.exclusiveMinimum)) { throw new Error( - `Integer ${integer} is below the exclusive minimum ${shape.exclusiveMinimum}.` + `Integer ${integer} is below the exclusive minimum ${shape.exclusiveMinimum}.`, ); } if (shape.exclusiveMaximum && integer >= BigInt(shape.exclusiveMaximum)) { throw new Error( - `Integer ${integer} is above the exclusive maximum ${shape.exclusiveMaximum}.` + `Integer ${integer} is above the exclusive maximum ${shape.exclusiveMaximum}.`, ); } } function bytesConstraints(bytes: string, shape: TSchema) { - if (shape.enum && !shape.enum.some((keyword: string) => keyword === bytes)) + if (shape.enum && !shape.enum.some((keyword: string) => keyword === bytes)) { throw new Error(`None of the keywords match with '${bytes}'.`); + } if (shape.minLength && bytes.length / 2 < shape.minLength) { throw new Error( - `Bytes need to have a length of at least ${shape.minLength} bytes.` + `Bytes need to have a length of at least ${shape.minLength} bytes.`, ); } if (shape.maxLength && bytes.length / 2 > shape.maxLength) { throw new Error( - `Bytes can have a length of at most ${shape.minLength} bytes.` + `Bytes can have a length of at most ${shape.minLength} bytes.`, ); } } From d9cc706fc7a1dc3f8b11126f4f143f23bfa008c9 Mon Sep 17 00:00:00 2001 From: yHSJ Date: Thu, 2 Nov 2023 12:07:00 -0400 Subject: [PATCH 03/54] chore: update freeable types --- src/plutus/data.ts | 133 +++++++++++++++++++++--------------------- src/utils/freeable.ts | 4 +- 2 files changed, 68 insertions(+), 69 deletions(-) diff --git a/src/plutus/data.ts b/src/plutus/data.ts index 63e028ca..1cbf3a8b 100644 --- a/src/plutus/data.ts +++ b/src/plutus/data.ts @@ -10,7 +10,7 @@ import { import { C } from "../core/mod.ts"; import { Datum, Exact, Json, Redeemer } from "../types/mod.ts"; import { fromHex, fromText, toHex } from "../utils/utils.ts"; -import { Freeable, Freeables } from "../utils/freeable.ts"; +import { FreeableBucket, Freeables } from "../utils/freeable.ts"; export class Constr { index: number; @@ -89,7 +89,7 @@ export const Data = { }, Array: function ( items: T, - options?: { minItems?: number; maxItems?: number; uniqueItems?: boolean }, + options?: { minItems?: number; maxItems?: number; uniqueItems?: boolean } ) { const array = Type.Array(items); replaceProperties(array, { dataType: "list", items }); @@ -103,7 +103,7 @@ export const Data = { Map: function ( keys: T, values: U, - options?: { minItems?: number; maxItems?: number }, + options?: { minItems?: number; maxItems?: number } ) { const map = Type.Unsafe, Data.Static>>({ dataType: "map", @@ -123,7 +123,7 @@ export const Data = { */ Object: function ( properties: T, - options?: { hasConstr?: boolean }, + options?: { hasConstr?: boolean } ) { const object = Type.Object(properties); replaceProperties(object, { @@ -138,8 +138,8 @@ export const Data = { }, ], }); - object.anyOf[0].hasConstr = typeof options?.hasConstr === "undefined" || - options.hasConstr; + object.anyOf[0].hasConstr = + typeof options?.hasConstr === "undefined" || options.hasConstr; return object; }, Enum: function (items: T[]) { @@ -148,27 +148,28 @@ export const Data = { anyOf: items.map((item, index) => item.anyOf[0].fields.length === 0 ? { - ...item.anyOf[0], - index, - } + ...item.anyOf[0], + index, + } : { - dataType: "constructor", - title: (() => { - const title = item.anyOf[0].fields[0].title; - if ( - (title as string).charAt(0) !== + dataType: "constructor", + title: (() => { + const title = item.anyOf[0].fields[0].title; + if ( + (title as string).charAt(0) !== (title as string).charAt(0).toUpperCase() - ) { - throw new Error( - `Enum '${title}' needs to start with an uppercase letter.`, - ); - } - return item.anyOf[0].fields[0].title; - })(), - index, - fields: item.anyOf[0].fields[0].items || - item.anyOf[0].fields[0].anyOf[0].fields, - } + ) { + throw new Error( + `Enum '${title}' needs to start with an uppercase letter.` + ); + } + return item.anyOf[0].fields[0].title; + })(), + index, + fields: + item.anyOf[0].fields[0].items || + item.anyOf[0].fields[0].anyOf[0].fields, + } ), }); return union; @@ -179,7 +180,7 @@ export const Data = { */ Tuple: function ( items: [...T], - options?: { hasConstr?: boolean }, + options?: { hasConstr?: boolean } ) { const tuple = Type.Tuple(items); replaceProperties(tuple, { @@ -198,7 +199,7 @@ export const Data = { (title as string).charAt(0) !== (title as string).charAt(0).toUpperCase() ) { throw new Error( - `Enum '${title}' needs to start with an uppercase letter.`, + `Enum '${title}' needs to start with an uppercase letter.` ); } const literal = Type.Literal(title); @@ -278,8 +279,8 @@ function to(data: Exact, type?: T): Datum | Redeemer { return C.PlutusData.new_constr_plutus_data( C.ConstrPlutusData.new( C.BigNum.from_str(index.toString()), - plutusList, - ), + plutusList + ) ); } else if (data instanceof Array) { const plutusList = C.PlutusList.new(); @@ -311,7 +312,7 @@ function to(data: Exact, type?: T): Datum | Redeemer { */ function from(raw: Datum | Redeemer, type?: T): T { function deserialize(data: C.PlutusData): Data { - const bucket: Freeable[] = []; + const bucket: FreeableBucket = []; try { if (data.kind() === 0) { const constr = data.as_constr_plutus_data()!; @@ -408,15 +409,14 @@ function toJson(plutusData: Data): Json { !isNaN(parseInt(data)) && data.slice(-1) === "n") ) { - const bigint = typeof data === "string" - ? BigInt(data.slice(0, -1)) - : data; + const bigint = + typeof data === "string" ? BigInt(data.slice(0, -1)) : data; return parseInt(bigint.toString()); } if (typeof data === "string") { try { return new TextDecoder(undefined, { fatal: true }).decode( - fromHex(data), + fromHex(data) ); } catch (_) { return "0x" + toHex(fromHex(data)); @@ -432,7 +432,7 @@ function toJson(plutusData: Data): Json { typeof convertedKey !== "number" ) { throw new Error( - "Unsupported type (Note: Only bytes or integers can be keys of a JSON object)", + "Unsupported type (Note: Only bytes or integers can be keys of a JSON object)" ); } tempJson[convertedKey] = fromData(value); @@ -440,7 +440,7 @@ function toJson(plutusData: Data): Json { return tempJson; } throw new Error( - "Unsupported type (Note: Constructor cannot be converted to JSON)", + "Unsupported type (Note: Constructor cannot be converted to JSON)" ); } return fromData(plutusData); @@ -484,14 +484,14 @@ function castFrom(data: Data, type: T): T { const fields: Record = {}; if (shape.fields.length !== data.fields.length) { throw new Error( - "Could not type cast to object. Fields do not match.", + "Could not type cast to object. Fields do not match." ); } shape.fields.forEach((field: Json, fieldIndex: number) => { const title = field.title || "wrapper"; if (/[A-Z]/.test(title[0])) { throw new Error( - "Could not type cast to object. Object properties need to start with a lowercase letter.", + "Could not type cast to object. Object properties need to start with a lowercase letter." ); } fields[title] = castFrom(data.fields[fieldIndex], field); @@ -510,7 +510,7 @@ function castFrom(data: Data, type: T): T { const title = field.title || "wrapper"; if (/[A-Z]/.test(title[0])) { throw new Error( - "Could not type cast to object. Object properties need to start with a lowercase letter.", + "Could not type cast to object. Object properties need to start with a lowercase letter." ); } fields[title] = castFrom(data[fieldIndex], field); @@ -530,7 +530,7 @@ function castFrom(data: Data, type: T): T { } const enumShape = shape.anyOf.find( - (entry: Json) => entry.index === data.index, + (entry: Json) => entry.index === data.index ); if (!enumShape || enumShape.fields.length !== data.fields.length) { throw new Error("Could not type cast to enum."); @@ -573,7 +573,7 @@ function castFrom(data: Data, type: T): T { } else { if (!/[A-Z]/.test(enumShape.title)) { throw new Error( - "Could not type cast to enum. Enums need to start with an uppercase letter.", + "Could not type cast to enum. Enums need to start with an uppercase letter." ); } @@ -584,14 +584,14 @@ function castFrom(data: Data, type: T): T { // check if named args const args = enumShape.fields[0].title ? Object.fromEntries( - enumShape.fields.map((field: Json, index: number) => [ - field.title, - castFrom(data.fields[index], field), - ]), - ) + enumShape.fields.map((field: Json, index: number) => [ + field.title, + castFrom(data.fields[index], field), + ]) + ) : enumShape.fields.map((field: Json, index: number) => - castFrom(data.fields[index], field) - ); + castFrom(data.fields[index], field) + ); return { [enumShape.title]: args, @@ -679,7 +679,7 @@ function castTo(struct: Exact, type: T): Data { const fields = shape.fields.map((field: Json) => castTo( (struct as Record)[field.title || "wrapper"], - field, + field ) ); return shape.hasConstr || shape.hasConstr === undefined @@ -711,14 +711,14 @@ function castTo(struct: Exact, type: T): Data { case "string": { if (!/[A-Z]/.test(struct[0])) { throw new Error( - "Could not type cast to enum. Enum needs to start with an uppercase letter.", + "Could not type cast to enum. Enum needs to start with an uppercase letter." ); } const enumIndex = (shape as TEnum).anyOf.findIndex( (s: TLiteral) => s.dataType === "constructor" && s.fields.length === 0 && - s.title === struct, + s.title === struct ); if (enumIndex === -1) throw new Error("Could not type cast to enum."); return new Constr(enumIndex, []); @@ -729,12 +729,11 @@ function castTo(struct: Exact, type: T): Data { if (!/[A-Z]/.test(structTitle)) { throw new Error( - "Could not type cast to enum. Enum needs to start with an uppercase letter.", + "Could not type cast to enum. Enum needs to start with an uppercase letter." ); } const enumEntry = shape.anyOf.find( - (s: Json) => - s.dataType === "constructor" && s.title === structTitle, + (s: Json) => s.dataType === "constructor" && s.title === structTitle ); if (!enumEntry) throw new Error("Could not type cast to enum."); @@ -746,14 +745,14 @@ function castTo(struct: Exact, type: T): Data { // check if named args args instanceof Array ? args.map((item, index) => - castTo(item, enumEntry.fields[index]) - ) + castTo(item, enumEntry.fields[index]) + ) : enumEntry.fields.map((entry: Json) => { - const [_, item]: [string, Json] = Object.entries(args).find( - ([title]) => title === entry.title, - )!; - return castTo(item, entry); - }), + const [_, item]: [string, Json] = Object.entries(args).find( + ([title]) => title === entry.title + )!; + return castTo(item, entry); + }) ); } } @@ -798,22 +797,22 @@ function castTo(struct: Exact, type: T): Data { function integerConstraints(integer: bigint, shape: TSchema) { if (shape.minimum && integer < BigInt(shape.minimum)) { throw new Error( - `Integer ${integer} is below the minimum ${shape.minimum}.`, + `Integer ${integer} is below the minimum ${shape.minimum}.` ); } if (shape.maximum && integer > BigInt(shape.maximum)) { throw new Error( - `Integer ${integer} is above the maxiumum ${shape.maximum}.`, + `Integer ${integer} is above the maxiumum ${shape.maximum}.` ); } if (shape.exclusiveMinimum && integer <= BigInt(shape.exclusiveMinimum)) { throw new Error( - `Integer ${integer} is below the exclusive minimum ${shape.exclusiveMinimum}.`, + `Integer ${integer} is below the exclusive minimum ${shape.exclusiveMinimum}.` ); } if (shape.exclusiveMaximum && integer >= BigInt(shape.exclusiveMaximum)) { throw new Error( - `Integer ${integer} is above the exclusive maximum ${shape.exclusiveMaximum}.`, + `Integer ${integer} is above the exclusive maximum ${shape.exclusiveMaximum}.` ); } } @@ -824,13 +823,13 @@ function bytesConstraints(bytes: string, shape: TSchema) { } if (shape.minLength && bytes.length / 2 < shape.minLength) { throw new Error( - `Bytes need to have a length of at least ${shape.minLength} bytes.`, + `Bytes need to have a length of at least ${shape.minLength} bytes.` ); } if (shape.maxLength && bytes.length / 2 > shape.maxLength) { throw new Error( - `Bytes can have a length of at most ${shape.minLength} bytes.`, + `Bytes can have a length of at most ${shape.minLength} bytes.` ); } } diff --git a/src/utils/freeable.ts b/src/utils/freeable.ts index 651f5835..fc450684 100644 --- a/src/utils/freeable.ts +++ b/src/utils/freeable.ts @@ -2,10 +2,10 @@ export interface Freeable { free(): void; } -export type FreeableBucket = Array; +export type FreeableBucket = Array; export abstract class Freeables { - static free(...bucket: (Freeable | undefined)[]) { + static free(...bucket: FreeableBucket) { bucket.forEach((freeable) => { freeable?.free(); }); From b947eea5d47f1a4aa4432c875060b3d57b3d868f Mon Sep 17 00:00:00 2001 From: yHSJ Date: Thu, 2 Nov 2023 12:12:08 -0400 Subject: [PATCH 04/54] chore: add context to the freeables types --- src/plutus/data.ts | 129 +++++++++++++++++++++--------------------- src/utils/freeable.ts | 20 +++++++ 2 files changed, 85 insertions(+), 64 deletions(-) diff --git a/src/plutus/data.ts b/src/plutus/data.ts index 1cbf3a8b..92225b72 100644 --- a/src/plutus/data.ts +++ b/src/plutus/data.ts @@ -89,7 +89,7 @@ export const Data = { }, Array: function ( items: T, - options?: { minItems?: number; maxItems?: number; uniqueItems?: boolean } + options?: { minItems?: number; maxItems?: number; uniqueItems?: boolean }, ) { const array = Type.Array(items); replaceProperties(array, { dataType: "list", items }); @@ -103,7 +103,7 @@ export const Data = { Map: function ( keys: T, values: U, - options?: { minItems?: number; maxItems?: number } + options?: { minItems?: number; maxItems?: number }, ) { const map = Type.Unsafe, Data.Static>>({ dataType: "map", @@ -123,7 +123,7 @@ export const Data = { */ Object: function ( properties: T, - options?: { hasConstr?: boolean } + options?: { hasConstr?: boolean }, ) { const object = Type.Object(properties); replaceProperties(object, { @@ -138,8 +138,8 @@ export const Data = { }, ], }); - object.anyOf[0].hasConstr = - typeof options?.hasConstr === "undefined" || options.hasConstr; + object.anyOf[0].hasConstr = typeof options?.hasConstr === "undefined" || + options.hasConstr; return object; }, Enum: function (items: T[]) { @@ -148,28 +148,27 @@ export const Data = { anyOf: items.map((item, index) => item.anyOf[0].fields.length === 0 ? { - ...item.anyOf[0], - index, - } + ...item.anyOf[0], + index, + } : { - dataType: "constructor", - title: (() => { - const title = item.anyOf[0].fields[0].title; - if ( - (title as string).charAt(0) !== + dataType: "constructor", + title: (() => { + const title = item.anyOf[0].fields[0].title; + if ( + (title as string).charAt(0) !== (title as string).charAt(0).toUpperCase() - ) { - throw new Error( - `Enum '${title}' needs to start with an uppercase letter.` - ); - } - return item.anyOf[0].fields[0].title; - })(), - index, - fields: - item.anyOf[0].fields[0].items || - item.anyOf[0].fields[0].anyOf[0].fields, - } + ) { + throw new Error( + `Enum '${title}' needs to start with an uppercase letter.`, + ); + } + return item.anyOf[0].fields[0].title; + })(), + index, + fields: item.anyOf[0].fields[0].items || + item.anyOf[0].fields[0].anyOf[0].fields, + } ), }); return union; @@ -180,7 +179,7 @@ export const Data = { */ Tuple: function ( items: [...T], - options?: { hasConstr?: boolean } + options?: { hasConstr?: boolean }, ) { const tuple = Type.Tuple(items); replaceProperties(tuple, { @@ -199,7 +198,7 @@ export const Data = { (title as string).charAt(0) !== (title as string).charAt(0).toUpperCase() ) { throw new Error( - `Enum '${title}' needs to start with an uppercase letter.` + `Enum '${title}' needs to start with an uppercase letter.`, ); } const literal = Type.Literal(title); @@ -279,8 +278,8 @@ function to(data: Exact, type?: T): Datum | Redeemer { return C.PlutusData.new_constr_plutus_data( C.ConstrPlutusData.new( C.BigNum.from_str(index.toString()), - plutusList - ) + plutusList, + ), ); } else if (data instanceof Array) { const plutusList = C.PlutusList.new(); @@ -409,14 +408,15 @@ function toJson(plutusData: Data): Json { !isNaN(parseInt(data)) && data.slice(-1) === "n") ) { - const bigint = - typeof data === "string" ? BigInt(data.slice(0, -1)) : data; + const bigint = typeof data === "string" + ? BigInt(data.slice(0, -1)) + : data; return parseInt(bigint.toString()); } if (typeof data === "string") { try { return new TextDecoder(undefined, { fatal: true }).decode( - fromHex(data) + fromHex(data), ); } catch (_) { return "0x" + toHex(fromHex(data)); @@ -432,7 +432,7 @@ function toJson(plutusData: Data): Json { typeof convertedKey !== "number" ) { throw new Error( - "Unsupported type (Note: Only bytes or integers can be keys of a JSON object)" + "Unsupported type (Note: Only bytes or integers can be keys of a JSON object)", ); } tempJson[convertedKey] = fromData(value); @@ -440,7 +440,7 @@ function toJson(plutusData: Data): Json { return tempJson; } throw new Error( - "Unsupported type (Note: Constructor cannot be converted to JSON)" + "Unsupported type (Note: Constructor cannot be converted to JSON)", ); } return fromData(plutusData); @@ -484,14 +484,14 @@ function castFrom(data: Data, type: T): T { const fields: Record = {}; if (shape.fields.length !== data.fields.length) { throw new Error( - "Could not type cast to object. Fields do not match." + "Could not type cast to object. Fields do not match.", ); } shape.fields.forEach((field: Json, fieldIndex: number) => { const title = field.title || "wrapper"; if (/[A-Z]/.test(title[0])) { throw new Error( - "Could not type cast to object. Object properties need to start with a lowercase letter." + "Could not type cast to object. Object properties need to start with a lowercase letter.", ); } fields[title] = castFrom(data.fields[fieldIndex], field); @@ -510,7 +510,7 @@ function castFrom(data: Data, type: T): T { const title = field.title || "wrapper"; if (/[A-Z]/.test(title[0])) { throw new Error( - "Could not type cast to object. Object properties need to start with a lowercase letter." + "Could not type cast to object. Object properties need to start with a lowercase letter.", ); } fields[title] = castFrom(data[fieldIndex], field); @@ -530,7 +530,7 @@ function castFrom(data: Data, type: T): T { } const enumShape = shape.anyOf.find( - (entry: Json) => entry.index === data.index + (entry: Json) => entry.index === data.index, ); if (!enumShape || enumShape.fields.length !== data.fields.length) { throw new Error("Could not type cast to enum."); @@ -573,7 +573,7 @@ function castFrom(data: Data, type: T): T { } else { if (!/[A-Z]/.test(enumShape.title)) { throw new Error( - "Could not type cast to enum. Enums need to start with an uppercase letter." + "Could not type cast to enum. Enums need to start with an uppercase letter.", ); } @@ -584,14 +584,14 @@ function castFrom(data: Data, type: T): T { // check if named args const args = enumShape.fields[0].title ? Object.fromEntries( - enumShape.fields.map((field: Json, index: number) => [ - field.title, - castFrom(data.fields[index], field), - ]) - ) + enumShape.fields.map((field: Json, index: number) => [ + field.title, + castFrom(data.fields[index], field), + ]), + ) : enumShape.fields.map((field: Json, index: number) => - castFrom(data.fields[index], field) - ); + castFrom(data.fields[index], field) + ); return { [enumShape.title]: args, @@ -679,7 +679,7 @@ function castTo(struct: Exact, type: T): Data { const fields = shape.fields.map((field: Json) => castTo( (struct as Record)[field.title || "wrapper"], - field + field, ) ); return shape.hasConstr || shape.hasConstr === undefined @@ -711,14 +711,14 @@ function castTo(struct: Exact, type: T): Data { case "string": { if (!/[A-Z]/.test(struct[0])) { throw new Error( - "Could not type cast to enum. Enum needs to start with an uppercase letter." + "Could not type cast to enum. Enum needs to start with an uppercase letter.", ); } const enumIndex = (shape as TEnum).anyOf.findIndex( (s: TLiteral) => s.dataType === "constructor" && s.fields.length === 0 && - s.title === struct + s.title === struct, ); if (enumIndex === -1) throw new Error("Could not type cast to enum."); return new Constr(enumIndex, []); @@ -729,11 +729,12 @@ function castTo(struct: Exact, type: T): Data { if (!/[A-Z]/.test(structTitle)) { throw new Error( - "Could not type cast to enum. Enum needs to start with an uppercase letter." + "Could not type cast to enum. Enum needs to start with an uppercase letter.", ); } const enumEntry = shape.anyOf.find( - (s: Json) => s.dataType === "constructor" && s.title === structTitle + (s: Json) => + s.dataType === "constructor" && s.title === structTitle, ); if (!enumEntry) throw new Error("Could not type cast to enum."); @@ -745,14 +746,14 @@ function castTo(struct: Exact, type: T): Data { // check if named args args instanceof Array ? args.map((item, index) => - castTo(item, enumEntry.fields[index]) - ) + castTo(item, enumEntry.fields[index]) + ) : enumEntry.fields.map((entry: Json) => { - const [_, item]: [string, Json] = Object.entries(args).find( - ([title]) => title === entry.title - )!; - return castTo(item, entry); - }) + const [_, item]: [string, Json] = Object.entries(args).find( + ([title]) => title === entry.title, + )!; + return castTo(item, entry); + }), ); } } @@ -797,22 +798,22 @@ function castTo(struct: Exact, type: T): Data { function integerConstraints(integer: bigint, shape: TSchema) { if (shape.minimum && integer < BigInt(shape.minimum)) { throw new Error( - `Integer ${integer} is below the minimum ${shape.minimum}.` + `Integer ${integer} is below the minimum ${shape.minimum}.`, ); } if (shape.maximum && integer > BigInt(shape.maximum)) { throw new Error( - `Integer ${integer} is above the maxiumum ${shape.maximum}.` + `Integer ${integer} is above the maxiumum ${shape.maximum}.`, ); } if (shape.exclusiveMinimum && integer <= BigInt(shape.exclusiveMinimum)) { throw new Error( - `Integer ${integer} is below the exclusive minimum ${shape.exclusiveMinimum}.` + `Integer ${integer} is below the exclusive minimum ${shape.exclusiveMinimum}.`, ); } if (shape.exclusiveMaximum && integer >= BigInt(shape.exclusiveMaximum)) { throw new Error( - `Integer ${integer} is above the exclusive maximum ${shape.exclusiveMaximum}.` + `Integer ${integer} is above the exclusive maximum ${shape.exclusiveMaximum}.`, ); } } @@ -823,13 +824,13 @@ function bytesConstraints(bytes: string, shape: TSchema) { } if (shape.minLength && bytes.length / 2 < shape.minLength) { throw new Error( - `Bytes need to have a length of at least ${shape.minLength} bytes.` + `Bytes need to have a length of at least ${shape.minLength} bytes.`, ); } if (shape.maxLength && bytes.length / 2 > shape.maxLength) { throw new Error( - `Bytes can have a length of at most ${shape.minLength} bytes.` + `Bytes can have a length of at most ${shape.minLength} bytes.`, ); } } diff --git a/src/utils/freeable.ts b/src/utils/freeable.ts index fc450684..99b2d215 100644 --- a/src/utils/freeable.ts +++ b/src/utils/freeable.ts @@ -1,9 +1,29 @@ +/** + * These types and classes are used to help with freeing memory. + * Objects passed from the WASM to JS (Objects from Rust libraries, for example) are not freed automatically, or at least inconsistently. + * This can lead to memory leaks. + * In order to free these objects, we need to call the `free()` method on them. These types make it easier. + */ + +/** This interface represents WASM objects that can and need to be freed. */ export interface Freeable { free(): void; } export type FreeableBucket = Array; +/** This class makes it easier to free large sets of memory. It can be used like this: + * ```ts + * const bucket: FreeableBucket = []; + * try { + * const rustObject = C.some_rust_object(); + * bucket.push(rustObject); + * ... + * } finally { + * Freeables.free(...bucket); + * } + * ``` + */ export abstract class Freeables { static free(...bucket: FreeableBucket) { bucket.forEach((freeable) => { From 3e89765137aad60f728fdce07544ed7283cb86c7 Mon Sep 17 00:00:00 2001 From: yHSJ Date: Wed, 1 Nov 2023 13:51:12 -0400 Subject: [PATCH 05/54] feat: free references in Lucid.new --- src/lucid/lucid.ts | 206 ++++++++++-------------- src/utils/cost_model.ts | 48 ++++-- src/utils/transaction_builder_config.ts | 108 +++++++++++++ 3 files changed, 224 insertions(+), 138 deletions(-) create mode 100644 src/utils/transaction_builder_config.ts diff --git a/src/lucid/lucid.ts b/src/lucid/lucid.ts index b886fbe2..e56d6a21 100644 --- a/src/lucid/lucid.ts +++ b/src/lucid/lucid.ts @@ -40,6 +40,7 @@ import { Message } from "./message.ts"; import { SLOT_CONFIG_NETWORK } from "../plutus/time.ts"; import { Constr, Data } from "../plutus/data.ts"; import { Emulator } from "../provider/emulator.ts"; +import { getTransactionBuilderConfig } from "../utils/transaction_builder_config.ts"; export class Lucid { txBuilderConfig!: C.TransactionBuilderConfig; @@ -65,54 +66,13 @@ export class Lucid { } const slotConfig = SLOT_CONFIG_NETWORK[lucid.network]; - lucid.txBuilderConfig = C.TransactionBuilderConfigBuilder.new() - .coins_per_utxo_byte( - C.BigNum.from_str(protocolParameters.coinsPerUtxoByte.toString()), - ) - .fee_algo( - C.LinearFee.new( - C.BigNum.from_str(protocolParameters.minFeeA.toString()), - C.BigNum.from_str(protocolParameters.minFeeB.toString()), - ), - ) - .key_deposit( - C.BigNum.from_str(protocolParameters.keyDeposit.toString()), - ) - .pool_deposit( - C.BigNum.from_str(protocolParameters.poolDeposit.toString()), - ) - .max_tx_size(protocolParameters.maxTxSize) - .max_value_size(protocolParameters.maxValSize) - .collateral_percentage(protocolParameters.collateralPercentage) - .max_collateral_inputs(protocolParameters.maxCollateralInputs) - .max_tx_ex_units( - C.ExUnits.new( - C.BigNum.from_str(protocolParameters.maxTxExMem.toString()), - C.BigNum.from_str(protocolParameters.maxTxExSteps.toString()), - ), - ) - .ex_unit_prices( - C.ExUnitPrices.from_float( - protocolParameters.priceMem, - protocolParameters.priceStep, - ), - ) - .slot_config( - C.BigNum.from_str(slotConfig.zeroTime.toString()), - C.BigNum.from_str(slotConfig.zeroSlot.toString()), - slotConfig.slotLength, - ) - .blockfrost( - // We have Aiken now as native plutus core engine (primary), but we still support blockfrost (secondary) in case of bugs. - C.Blockfrost.new( - // deno-lint-ignore no-explicit-any - ((provider as any)?.url || "") + "/utils/txs/evaluate", - // deno-lint-ignore no-explicit-any - (provider as any)?.projectId || "", - ), - ) - .costmdls(createCostModels(protocolParameters.costModels)) - .build(); + const txBuilderConfig = getTransactionBuilderConfig( + protocolParameters, + slotConfig, + // deno-lint-ignore no-explicit-any + { url: (provider as any)?.url, projectId: (provider as any)?.projectId } + ); + lucid.txBuilderConfig = txBuilderConfig; } lucid.utils = new Utils(lucid); return lucid; @@ -126,10 +86,7 @@ export class Lucid { if (this.network === "Custom") { throw new Error("Cannot switch when on custom network."); } - const lucid = await Lucid.new( - provider, - network, - ); + const lucid = await Lucid.new(provider, network); this.txBuilderConfig = lucid.txBuilderConfig; this.provider = provider || this.provider; this.network = network || this.network; @@ -154,12 +111,13 @@ export class Lucid { verifyMessage( address: Address | RewardAddress, payload: Payload, - signedMessage: SignedMessage, + signedMessage: SignedMessage ): boolean { - const { paymentCredential, stakeCredential, address: { hex: addressHex } } = - this.utils.getAddressDetails( - address, - ); + const { + paymentCredential, + stakeCredential, + address: { hex: addressHex }, + } = this.utils.getAddressDetails(address); const keyHash = paymentCredential?.hash || stakeCredential?.hash; if (!keyHash) throw new Error("Not a valid address provided."); @@ -176,7 +134,7 @@ export class Lucid { utxosAtWithUnit( addressOrCredential: Address | Credential, - unit: Unit, + unit: Unit ): Promise { return this.provider.getUtxosWithUnit(addressOrCredential, unit); } @@ -216,7 +174,7 @@ export class Lucid { case 333: case 444: { const utxo = await this.utxoByUnit(toUnit(policyId, name, 100)); - const metadata = await this.datumOf(utxo) as Constr; + const metadata = (await this.datumOf(utxo)) as Constr; return Data.toJson(metadata.fields[0]); } default: @@ -237,7 +195,7 @@ export class Lucid { address: async (): Promise
=> C.EnterpriseAddress.new( this.network === "Mainnet" ? 1 : 0, - C.StakeCredential.from_keyhash(pubKeyHash), + C.StakeCredential.from_keyhash(pubKeyHash) ) .to_address() .to_bech32(undefined), @@ -245,12 +203,12 @@ export class Lucid { rewardAddress: async (): Promise => null, getUtxos: async (): Promise => { return await this.utxosAt( - paymentCredentialOf(await this.wallet.address()), + paymentCredentialOf(await this.wallet.address()) ); }, getUtxosCore: async (): Promise => { const utxos = await this.utxosAt( - paymentCredentialOf(await this.wallet.address()), + paymentCredentialOf(await this.wallet.address()) ); const coreUtxos = C.TransactionUnspentOutputs.new(); utxos.forEach((utxo) => { @@ -263,12 +221,10 @@ export class Lucid { return { poolId: null, rewards: 0n }; }, // deno-lint-ignore require-await - signTx: async ( - tx: C.Transaction, - ): Promise => { + signTx: async (tx: C.Transaction): Promise => { const witness = C.make_vkey_witness( C.hash_transaction(tx.body()), - priv, + priv ); const txWitnessSetBuilder = C.TransactionWitnessSetBuilder.new(); txWitnessSetBuilder.add_vkey(witness); @@ -277,10 +233,12 @@ export class Lucid { // deno-lint-ignore require-await signMessage: async ( address: Address | RewardAddress, - payload: Payload, + payload: Payload ): Promise => { - const { paymentCredential, address: { hex: hexAddress } } = this.utils - .getAddressDetails(address); + const { + paymentCredential, + address: { hex: hexAddress }, + } = this.utils.getAddressDetails(address); const keyHash = paymentCredential?.hash; const originalKeyHash = pubKeyHash.to_hex(); @@ -309,24 +267,24 @@ export class Lucid { this.wallet = { address: async (): Promise
=> - C.Address.from_bytes( - fromHex(await getAddressHex()), - ).to_bech32(undefined), + C.Address.from_bytes(fromHex(await getAddressHex())).to_bech32( + undefined + ), rewardAddress: async (): Promise => { const [rewardAddressHex] = await api.getRewardAddresses(); const rewardAddress = rewardAddressHex ? C.RewardAddress.from_address( - C.Address.from_bytes(fromHex(rewardAddressHex)), - )! - .to_address() - .to_bech32(undefined) + C.Address.from_bytes(fromHex(rewardAddressHex)) + )! + .to_address() + .to_bech32(undefined) : null; return rewardAddress; }, getUtxos: async (): Promise => { const utxos = ((await api.getUtxos()) || []).map((utxo) => { const parsedUtxo = C.TransactionUnspentOutput.from_bytes( - fromHex(utxo), + fromHex(utxo) ); return coreToUtxo(parsedUtxo); }); @@ -346,15 +304,13 @@ export class Lucid { ? await this.delegationAt(rewardAddr) : { poolId: null, rewards: 0n }; }, - signTx: async ( - tx: C.Transaction, - ): Promise => { + signTx: async (tx: C.Transaction): Promise => { const witnessSet = await api.signTx(toHex(tx.to_bytes()), true); return C.TransactionWitnessSet.from_bytes(fromHex(witnessSet)); }, signMessage: async ( address: Address | RewardAddress, - payload: Payload, + payload: Payload ): Promise => { const hexAddress = toHex(C.Address.from_bech32(address).to_bytes()); return await api.signData(hexAddress, payload); @@ -371,41 +327,38 @@ export class Lucid { * Emulates a wallet by constructing it with the utxos and an address. * If utxos are not set, utxos are fetched from the provided address. */ - selectWalletFrom({ - address, - utxos, - rewardAddress, - }: ExternalWallet): Lucid { + selectWalletFrom({ address, utxos, rewardAddress }: ExternalWallet): Lucid { const addressDetails = this.utils.getAddressDetails(address); this.wallet = { // deno-lint-ignore require-await address: async (): Promise
=> address, // deno-lint-ignore require-await rewardAddress: async (): Promise => { - const rewardAddr = !rewardAddress && addressDetails.stakeCredential - ? (() => { - if (addressDetails.stakeCredential.type === "Key") { - return C.RewardAddress.new( - this.network === "Mainnet" ? 1 : 0, - C.StakeCredential.from_keyhash( - C.Ed25519KeyHash.from_hex( - addressDetails.stakeCredential.hash, - ), - ), - ) - .to_address() - .to_bech32(undefined); - } - return C.RewardAddress.new( - this.network === "Mainnet" ? 1 : 0, - C.StakeCredential.from_scripthash( - C.ScriptHash.from_hex(addressDetails.stakeCredential.hash), - ), - ) - .to_address() - .to_bech32(undefined); - })() - : rewardAddress; + const rewardAddr = + !rewardAddress && addressDetails.stakeCredential + ? (() => { + if (addressDetails.stakeCredential.type === "Key") { + return C.RewardAddress.new( + this.network === "Mainnet" ? 1 : 0, + C.StakeCredential.from_keyhash( + C.Ed25519KeyHash.from_hex( + addressDetails.stakeCredential.hash + ) + ) + ) + .to_address() + .to_bech32(undefined); + } + return C.RewardAddress.new( + this.network === "Mainnet" ? 1 : 0, + C.StakeCredential.from_scripthash( + C.ScriptHash.from_hex(addressDetails.stakeCredential.hash) + ) + ) + .to_address() + .to_bech32(undefined); + })() + : rewardAddress; return rewardAddr || null; }, getUtxos: async (): Promise => { @@ -413,8 +366,10 @@ export class Lucid { }, getUtxosCore: async (): Promise => { const coreUtxos = C.TransactionUnspentOutputs.new(); - (utxos ? utxos : await this.utxosAt(paymentCredentialOf(address))) - .forEach((utxo) => coreUtxos.add(utxoToCore(utxo))); + (utxos + ? utxos + : await this.utxosAt(paymentCredentialOf(address)) + ).forEach((utxo) => coreUtxos.add(utxoToCore(utxo))); return coreUtxos; }, getDelegation: async (): Promise => { @@ -449,7 +404,7 @@ export class Lucid { addressType?: "Base" | "Enterprise"; accountIndex?: number; password?: string; - }, + } ): Lucid { const { address, rewardAddress, paymentKey, stakeKey } = walletFromSeed( seed, @@ -458,11 +413,13 @@ export class Lucid { accountIndex: options?.accountIndex || 0, password: options?.password, network: this.network, - }, + } ); - const paymentKeyHash = C.PrivateKey.from_bech32(paymentKey).to_public() - .hash().to_hex(); + const paymentKeyHash = C.PrivateKey.from_bech32(paymentKey) + .to_public() + .hash() + .to_hex(); const stakeKeyHash = stakeKey ? C.PrivateKey.from_bech32(stakeKey).to_public().hash().to_hex() : ""; @@ -495,9 +452,7 @@ export class Lucid { ? await this.delegationAt(rewardAddr) : { poolId: null, rewards: 0n }; }, - signTx: async ( - tx: C.Transaction, - ): Promise => { + signTx: async (tx: C.Transaction): Promise => { const utxos = await this.utxosAt(address); const ownKeyHashes: Array = [paymentKeyHash, stakeKeyHash]; @@ -505,14 +460,14 @@ export class Lucid { const usedKeyHashes = discoverOwnUsedTxKeyHashes( tx, ownKeyHashes, - utxos, + utxos ); const txWitnessSetBuilder = C.TransactionWitnessSetBuilder.new(); usedKeyHashes.forEach((keyHash) => { const witness = C.make_vkey_witness( C.hash_transaction(tx.body()), - C.PrivateKey.from_bech32(privKeyHashMap[keyHash]!), + C.PrivateKey.from_bech32(privKeyHashMap[keyHash]!) ); txWitnessSetBuilder.add_vkey(witness); }); @@ -521,14 +476,13 @@ export class Lucid { // deno-lint-ignore require-await signMessage: async ( address: Address | RewardAddress, - payload: Payload, + payload: Payload ): Promise => { const { paymentCredential, stakeCredential, address: { hex: hexAddress }, - } = this.utils - .getAddressDetails(address); + } = this.utils.getAddressDetails(address); const keyHash = paymentCredential?.hash || stakeCredential?.hash; @@ -546,4 +500,8 @@ export class Lucid { }; return this; } + + free() { + this.txBuilderConfig.free(); + } } diff --git a/src/utils/cost_model.ts b/src/utils/cost_model.ts index ed1e40db..62d8484b 100644 --- a/src/utils/cost_model.ts +++ b/src/utils/cost_model.ts @@ -1,25 +1,45 @@ import { C } from "../core/mod.ts"; import { CostModels } from "../mod.ts"; import { ProtocolParameters } from "../types/types.ts"; +import { Freeable, Freeables } from "./freeable.ts"; export function createCostModels(costModels: CostModels): C.Costmdls { - const costmdls = C.Costmdls.new(); + const bucket: Freeable[] = []; + try { + const costmdls = C.Costmdls.new(); - // add plutus v1 - const costmdlV1 = C.CostModel.new(); - Object.values(costModels.PlutusV1).forEach((cost, index) => { - costmdlV1.set(index, C.Int.new(C.BigNum.from_str(cost.toString()))); - }); - costmdls.insert(C.Language.new_plutus_v1(), costmdlV1); + // add plutus v1 + const costmdlV1 = C.CostModel.new(); + bucket.push(costmdlV1); + Object.values(costModels.PlutusV1).forEach((cost, index) => { + const bigNumVal = C.BigNum.from_str(cost.toString()); + bucket.push(bigNumVal); + const intVal = C.Int.new(bigNumVal); + bucket.push(intVal); + costmdlV1.set(index, intVal); + }); + const plutusV1 = C.Language.new_plutus_v1(); + bucket.push(plutusV1); + costmdls.insert(plutusV1, costmdlV1); - // add plutus v2 - const costmdlV2 = C.CostModel.new_plutus_v2(); - Object.values(costModels.PlutusV2 || []).forEach((cost, index) => { - costmdlV2.set(index, C.Int.new(C.BigNum.from_str(cost.toString()))); - }); - costmdls.insert(C.Language.new_plutus_v2(), costmdlV2); + // add plutus v2 + const costmdlV2 = C.CostModel.new_plutus_v2(); + bucket.push(costmdlV2); + Object.values(costModels.PlutusV2 || []).forEach((cost, index) => { + const bigNumVal = C.BigNum.from_str(cost.toString()); + bucket.push(bigNumVal); + const intVal = C.Int.new(bigNumVal); + bucket.push(intVal); + costmdlV2.set(index, intVal); + }); + const plutusV2 = C.Language.new_plutus_v2(); + bucket.push(plutusV2); + costmdls.insert(plutusV2, costmdlV2); - return costmdls; + return costmdls; + } finally { + Freeables.free(...bucket); + } } export const PROTOCOL_PARAMETERS_DEFAULT: ProtocolParameters = { diff --git a/src/utils/transaction_builder_config.ts b/src/utils/transaction_builder_config.ts new file mode 100644 index 00000000..dd9a5fb4 --- /dev/null +++ b/src/utils/transaction_builder_config.ts @@ -0,0 +1,108 @@ +import { C, SlotConfig } from "../mod.ts"; +import { ProtocolParameters } from "../types/mod.ts"; +import { createCostModels } from "./cost_model.ts"; +import { Freeable, Freeables } from "./freeable.ts"; + +export function getTransactionBuilderConfig( + protocolParameters: ProtocolParameters, + slotConfig: SlotConfig, + blockfrostConfig: { + url?: string; + projectId?: string; + } +) { + const bucket: Freeable[] = []; + let builderA = C.TransactionBuilderConfigBuilder.new(); + + const coinsPerUtxoByte = C.BigNum.from_str( + protocolParameters.coinsPerUtxoByte.toString() + ); + bucket.push(coinsPerUtxoByte); + let builderB = builderA.coins_per_utxo_byte(coinsPerUtxoByte); + builderA.free(); + + const minFeeA = C.BigNum.from_str(protocolParameters.minFeeA.toString()); + bucket.push(minFeeA); + const minFeeB = C.BigNum.from_str(protocolParameters.minFeeB.toString()); + bucket.push(minFeeB); + const linearFee = C.LinearFee.new(minFeeA, minFeeB); + bucket.push(linearFee); + builderA = builderB.fee_algo(linearFee); + builderB.free(); + + const keyDeposit = C.BigNum.from_str( + protocolParameters.keyDeposit.toString() + ); + bucket.push(keyDeposit); + builderB = builderA.key_deposit(keyDeposit); + builderA.free(); + + const poolDeposit = C.BigNum.from_str( + protocolParameters.poolDeposit.toString() + ); + bucket.push(poolDeposit); + builderA = builderB.pool_deposit(poolDeposit); + builderB.free(); + + builderB = builderA.max_tx_size(protocolParameters.maxTxSize); + builderA.free(); + + builderA = builderB.max_value_size(protocolParameters.maxValSize); + builderB.free(); + + builderB = builderA.collateral_percentage( + protocolParameters.collateralPercentage + ); + builderA.free(); + + builderA = builderB.max_collateral_inputs( + protocolParameters.maxCollateralInputs + ); + builderB.free(); + + const maxTxExMem = C.BigNum.from_str( + protocolParameters.maxTxExMem.toString() + ); + bucket.push(maxTxExMem); + const maxTxExSteps = C.BigNum.from_str( + protocolParameters.maxTxExSteps.toString() + ); + bucket.push(maxTxExSteps); + const exUnits = C.ExUnits.new(maxTxExMem, maxTxExSteps); + bucket.push(exUnits); + builderB = builderA.max_tx_ex_units(exUnits); + builderA.free(); + + const exUnitPrices = C.ExUnitPrices.from_float( + protocolParameters.priceMem, + protocolParameters.priceStep + ); + bucket.push(exUnitPrices); + builderA = builderB.ex_unit_prices(exUnitPrices); + builderB.free(); + + const zeroTime = C.BigNum.from_str(slotConfig.zeroTime.toString()); + bucket.push(zeroTime); + const zeroSlot = C.BigNum.from_str(slotConfig.zeroSlot.toString()); + bucket.push(zeroSlot); + builderB = builderA.slot_config(zeroTime, zeroSlot, slotConfig.slotLength); + builderA.free(); + + const blockfrost = C.Blockfrost.new( + blockfrostConfig?.url ?? "" + "utils/tx/evaulate", + blockfrostConfig?.projectId ?? "" + ); + bucket.push(blockfrost); + builderA = builderB.blockfrost(blockfrost); + builderB.free(); + + const costModels = createCostModels(protocolParameters.costModels); + bucket.push(costModels); + builderB = builderA.costmdls(costModels); + + const config = builderB.build(); + builderB.free(); + Freeables.free(...bucket); + + return config; +} From d74544e9f91811844d76cc38a7ea09a372903df6 Mon Sep 17 00:00:00 2001 From: yHSJ Date: Wed, 1 Nov 2023 13:52:48 -0400 Subject: [PATCH 06/54] feat: free references in Lucid.switchProvider --- src/lucid/lucid.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lucid/lucid.ts b/src/lucid/lucid.ts index e56d6a21..77d264c9 100644 --- a/src/lucid/lucid.ts +++ b/src/lucid/lucid.ts @@ -91,6 +91,8 @@ export class Lucid { this.provider = provider || this.provider; this.network = network || this.network; this.wallet = lucid.wallet; + + lucid.free(); return this; } From f3442e4937dacbcb334704f0a35b02a9da95618a Mon Sep 17 00:00:00 2001 From: yHSJ Date: Wed, 1 Nov 2023 14:06:54 -0400 Subject: [PATCH 07/54] feat: free references in Lucid.selectWalletFromPrivateKey --- src/lucid/lucid.ts | 65 ++++++++++++++++++++++++++++++---------------- 1 file changed, 42 insertions(+), 23 deletions(-) diff --git a/src/lucid/lucid.ts b/src/lucid/lucid.ts index 77d264c9..0b59012e 100644 --- a/src/lucid/lucid.ts +++ b/src/lucid/lucid.ts @@ -1,7 +1,6 @@ import { C } from "../core/mod.ts"; import { coreToUtxo, - createCostModels, fromHex, fromUnit, paymentCredentialOf, @@ -41,6 +40,7 @@ import { SLOT_CONFIG_NETWORK } from "../plutus/time.ts"; import { Constr, Data } from "../plutus/data.ts"; import { Emulator } from "../provider/emulator.ts"; import { getTransactionBuilderConfig } from "../utils/transaction_builder_config.ts"; +import { Freeable, Freeables } from "../utils/freeable.ts"; export class Lucid { txBuilderConfig!: C.TransactionBuilderConfig; @@ -189,20 +189,33 @@ export class Lucid { * Only an Enteprise address (without stake credential) is derived. */ selectWalletFromPrivateKey(privateKey: PrivateKey): Lucid { + const bucket: Freeable[] = []; const priv = C.PrivateKey.from_bech32(privateKey); + bucket.push(priv); + const publicKey = priv.to_public(); + bucket.push(publicKey); const pubKeyHash = priv.to_public().hash(); + bucket.push(pubKeyHash); this.wallet = { - // deno-lint-ignore require-await - address: async (): Promise
=> - C.EnterpriseAddress.new( + address: (): Promise
=> { + const bucket: Freeable[] = []; + const stakeCredential = C.StakeCredential.from_keyhash(pubKeyHash); + bucket.push(stakeCredential); + const enterpriseAddress = C.EnterpriseAddress.new( this.network === "Mainnet" ? 1 : 0, - C.StakeCredential.from_keyhash(pubKeyHash) - ) - .to_address() - .to_bech32(undefined), - // deno-lint-ignore require-await - rewardAddress: async (): Promise => null, + stakeCredential + ); + bucket.push(enterpriseAddress); + const address = enterpriseAddress.to_address(); + bucket.push(address); + const bech32 = address.to_bech32(undefined); + Freeables.free(...bucket); + + return Promise.resolve(bech32); + }, + + rewardAddress: (): Promise => Promise.resolve(null), getUtxos: async (): Promise => { return await this.utxosAt( paymentCredentialOf(await this.wallet.address()) @@ -218,22 +231,26 @@ export class Lucid { }); return coreUtxos; }, - // deno-lint-ignore require-await - getDelegation: async (): Promise => { - return { poolId: null, rewards: 0n }; + getDelegation: (): Promise => { + return Promise.resolve({ poolId: null, rewards: 0n }); }, - // deno-lint-ignore require-await - signTx: async (tx: C.Transaction): Promise => { - const witness = C.make_vkey_witness( - C.hash_transaction(tx.body()), - priv - ); + signTx: (tx: C.Transaction): Promise => { + const bucket: Freeable[] = []; + const txBody = tx.body(); + bucket.push(txBody); + const hash = C.hash_transaction(txBody); + bucket.push(hash); + const witness = C.make_vkey_witness(hash, priv); + bucket.push(witness); const txWitnessSetBuilder = C.TransactionWitnessSetBuilder.new(); + bucket.push(txWitnessSetBuilder); txWitnessSetBuilder.add_vkey(witness); - return txWitnessSetBuilder.build(); + const witnessSet = txWitnessSetBuilder.build(); + + Freeables.free(...bucket); + return Promise.resolve(witnessSet); }, - // deno-lint-ignore require-await - signMessage: async ( + signMessage: ( address: Address | RewardAddress, payload: Payload ): Promise => { @@ -249,12 +266,14 @@ export class Lucid { throw new Error(`Cannot sign message for address: ${address}.`); } - return signData(hexAddress, payload, privateKey); + return Promise.resolve(signData(hexAddress, payload, privateKey)); }, submitTx: async (tx: Transaction): Promise => { return await this.provider.submitTx(tx); }, }; + + Freeables.free(...bucket); return this; } From fda53f1590fd61a9b62b1c0e9208ff83f7eafc2a Mon Sep 17 00:00:00 2001 From: yHSJ Date: Wed, 1 Nov 2023 14:13:24 -0400 Subject: [PATCH 08/54] feat: free memory in Lucid.selectWallet --- src/lucid/lucid.ts | 43 ++++++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/src/lucid/lucid.ts b/src/lucid/lucid.ts index 0b59012e..69b9101a 100644 --- a/src/lucid/lucid.ts +++ b/src/lucid/lucid.ts @@ -287,34 +287,45 @@ export class Lucid { }; this.wallet = { - address: async (): Promise
=> - C.Address.from_bytes(fromHex(await getAddressHex())).to_bech32( - undefined - ), + address: async (): Promise
=> { + const addressHex = await getAddressHex(); + const address = C.Address.from_bytes(fromHex(addressHex)); + const bech32 = address.to_bech32(undefined); + address.free(); + return bech32; + }, + rewardAddress: async (): Promise => { const [rewardAddressHex] = await api.getRewardAddresses(); - const rewardAddress = rewardAddressHex - ? C.RewardAddress.from_address( - C.Address.from_bytes(fromHex(rewardAddressHex)) - )! - .to_address() - .to_bech32(undefined) - : null; - return rewardAddress; + if (rewardAddressHex) { + const address = C.Address.from_bytes(fromHex(rewardAddressHex)); + const rewardAddress = C.RewardAddress.from_address(address)!; + address.free(); + const addr = rewardAddress.to_address(); + rewardAddress.free(); + const bech32 = addr.to_bech32(undefined); + addr.free(); + return bech32; + } + return null; }, getUtxos: async (): Promise => { const utxos = ((await api.getUtxos()) || []).map((utxo) => { const parsedUtxo = C.TransactionUnspentOutput.from_bytes( fromHex(utxo) ); - return coreToUtxo(parsedUtxo); + const finalUtxo = coreToUtxo(parsedUtxo); + parsedUtxo.free(); + return finalUtxo; }); return utxos; }, getUtxosCore: async (): Promise => { const utxos = C.TransactionUnspentOutputs.new(); ((await api.getUtxos()) || []).forEach((utxo) => { - utxos.add(C.TransactionUnspentOutput.from_bytes(fromHex(utxo))); + const cUtxo = C.TransactionUnspentOutput.from_bytes(fromHex(utxo)); + utxos.add(cUtxo); + cUtxo.free(); }); return utxos; }, @@ -333,7 +344,9 @@ export class Lucid { address: Address | RewardAddress, payload: Payload ): Promise => { - const hexAddress = toHex(C.Address.from_bech32(address).to_bytes()); + const cAddress = C.Address.from_bech32(address); + const hexAddress = toHex(cAddress.to_bytes()); + cAddress.free(); return await api.signData(hexAddress, payload); }, submitTx: async (tx: Transaction): Promise => { From 953e714fc0a99d14164c6b227f1f1cd27bebb2ca Mon Sep 17 00:00:00 2001 From: yHSJ Date: Wed, 1 Nov 2023 14:36:11 -0400 Subject: [PATCH 09/54] feat: free memory in Lucid.selectWalletFrom --- src/lucid/lucid.ts | 78 +++++++++++++++++++++------------------------- 1 file changed, 36 insertions(+), 42 deletions(-) diff --git a/src/lucid/lucid.ts b/src/lucid/lucid.ts index 69b9101a..16b688e7 100644 --- a/src/lucid/lucid.ts +++ b/src/lucid/lucid.ts @@ -364,36 +364,29 @@ export class Lucid { selectWalletFrom({ address, utxos, rewardAddress }: ExternalWallet): Lucid { const addressDetails = this.utils.getAddressDetails(address); this.wallet = { - // deno-lint-ignore require-await - address: async (): Promise
=> address, - // deno-lint-ignore require-await - rewardAddress: async (): Promise => { - const rewardAddr = - !rewardAddress && addressDetails.stakeCredential - ? (() => { - if (addressDetails.stakeCredential.type === "Key") { - return C.RewardAddress.new( - this.network === "Mainnet" ? 1 : 0, - C.StakeCredential.from_keyhash( - C.Ed25519KeyHash.from_hex( - addressDetails.stakeCredential.hash - ) - ) - ) - .to_address() - .to_bech32(undefined); - } - return C.RewardAddress.new( - this.network === "Mainnet" ? 1 : 0, - C.StakeCredential.from_scripthash( - C.ScriptHash.from_hex(addressDetails.stakeCredential.hash) - ) - ) - .to_address() - .to_bech32(undefined); - })() - : rewardAddress; - return rewardAddr || null; + address: (): Promise
=> Promise.resolve(address), + rewardAddress: (): Promise => { + if (!rewardAddress && addressDetails.stakeCredential) { + if (addressDetails.stakeCredential.type === "Key") { + const keyHash = C.Ed25519KeyHash.from_hex( + addressDetails.stakeCredential.hash + ); + const stakeCredential = C.StakeCredential.from_keyhash(keyHash); + keyHash.free(); + const rewardAddress = C.RewardAddress.new( + this.network === "Mainnet" ? 1 : 0, + stakeCredential + ); + stakeCredential.free(); + const address = rewardAddress.to_address(); + rewardAddress.free(); + const bech32 = address.to_bech32(undefined); + address.free(); + return Promise.resolve(bech32); + } + } + + return Promise.resolve(rewardAddress ?? null); }, getUtxos: async (): Promise => { return utxos ? utxos : await this.utxosAt(paymentCredentialOf(address)); @@ -403,7 +396,11 @@ export class Lucid { (utxos ? utxos : await this.utxosAt(paymentCredentialOf(address)) - ).forEach((utxo) => coreUtxos.add(utxoToCore(utxo))); + ).forEach((utxo) => { + const coreUtxo = utxoToCore(utxo); + coreUtxos.add(coreUtxo); + coreUtxo.free(); + }); return coreUtxos; }, getDelegation: async (): Promise => { @@ -413,17 +410,14 @@ export class Lucid { ? await this.delegationAt(rewardAddr) : { poolId: null, rewards: 0n }; }, - // deno-lint-ignore require-await - signTx: async (): Promise => { - throw new Error("Not implemented"); - }, - // deno-lint-ignore require-await - signMessage: async (): Promise => { - throw new Error("Not implemented"); - }, - submitTx: async (tx: Transaction): Promise => { - return await this.provider.submitTx(tx); - }, + signTx: (): Promise => + Promise.reject("Not implemented"), + + signMessage: (): Promise => + Promise.reject("Not implemented"), + + submitTx: (tx: Transaction): Promise => + this.provider.submitTx(tx), }; return this; } From b295d46ec4fcca7435acd77d4b3fe174e09abbce Mon Sep 17 00:00:00 2001 From: yHSJ Date: Wed, 1 Nov 2023 14:46:29 -0400 Subject: [PATCH 10/54] feat: free memory in Lucid.selectWalletFromSeed --- src/lucid/lucid.ts | 71 ++++++++++++++++++++++++++++++---------------- 1 file changed, 46 insertions(+), 25 deletions(-) diff --git a/src/lucid/lucid.ts b/src/lucid/lucid.ts index 16b688e7..35e6d49c 100644 --- a/src/lucid/lucid.ts +++ b/src/lucid/lucid.ts @@ -434,6 +434,7 @@ export class Lucid { password?: string; } ): Lucid { + const bucket: Freeable[] = []; const { address, rewardAddress, paymentKey, stakeKey } = walletFromSeed( seed, { @@ -444,13 +445,26 @@ export class Lucid { } ); - const paymentKeyHash = C.PrivateKey.from_bech32(paymentKey) - .to_public() - .hash() - .to_hex(); - const stakeKeyHash = stakeKey - ? C.PrivateKey.from_bech32(stakeKey).to_public().hash().to_hex() - : ""; + const paymentPrivateKey = C.PrivateKey.from_bech32(paymentKey); + bucket.push(paymentPrivateKey); + const paymentPublicKey = paymentPrivateKey.to_public(); + bucket.push(paymentPublicKey); + const paymentPubKeyHash = paymentPublicKey.hash(); + bucket.push(paymentPubKeyHash); + const paymentKeyHash = paymentPubKeyHash.to_hex(); + + const getStakeKeyHash = (stakeKey: string) => { + const stakePrivateKey = C.PrivateKey.from_bech32(stakeKey); + bucket.push(stakePrivateKey); + const stakePublicKey = stakePrivateKey.to_public(); + bucket.push(stakePublicKey); + const stakePubKeyHash = stakePublicKey.hash(); + bucket.push(stakePubKeyHash); + const stakeKeyHash = stakePubKeyHash.to_hex(); + return stakeKeyHash; + }; + + const stakeKeyHash = stakeKey ? getStakeKeyHash(stakeKey) : ""; const privKeyHashMap = { [paymentKeyHash]: paymentKey, @@ -458,19 +472,18 @@ export class Lucid { }; this.wallet = { - // deno-lint-ignore require-await - address: async (): Promise
=> address, - // deno-lint-ignore require-await - rewardAddress: async (): Promise => - rewardAddress || null, - // deno-lint-ignore require-await - getUtxos: async (): Promise => + address: (): Promise
=> Promise.resolve(address), + rewardAddress: (): Promise => + Promise.resolve(rewardAddress || null), + getUtxos: (): Promise => this.utxosAt(paymentCredentialOf(address)), getUtxosCore: async (): Promise => { const coreUtxos = C.TransactionUnspentOutputs.new(); - (await this.utxosAt(paymentCredentialOf(address))).forEach((utxo) => - coreUtxos.add(utxoToCore(utxo)) - ); + (await this.utxosAt(paymentCredentialOf(address))).forEach((utxo) => { + const coreUtxo = utxoToCore(utxo); + coreUtxos.add(coreUtxo); + coreUtxos.free(); + }); return coreUtxos; }, getDelegation: async (): Promise => { @@ -493,16 +506,22 @@ export class Lucid { const txWitnessSetBuilder = C.TransactionWitnessSetBuilder.new(); usedKeyHashes.forEach((keyHash) => { - const witness = C.make_vkey_witness( - C.hash_transaction(tx.body()), - C.PrivateKey.from_bech32(privKeyHashMap[keyHash]!) - ); + const txBody = tx.body(); + const hash = C.hash_transaction(txBody); + txBody.free(); + const privateKey = C.PrivateKey.from_bech32(privKeyHashMap[keyHash]!); + const witness = C.make_vkey_witness(hash, privateKey); + hash.free(); + privateKey.free(); txWitnessSetBuilder.add_vkey(witness); + witness.free(); }); - return txWitnessSetBuilder.build(); + + const txWitnessSet = txWitnessSetBuilder.build(); + txWitnessSetBuilder.free(); + return txWitnessSet; }, - // deno-lint-ignore require-await - signMessage: async ( + signMessage: ( address: Address | RewardAddress, payload: Payload ): Promise => { @@ -520,12 +539,14 @@ export class Lucid { throw new Error(`Cannot sign message for address: ${address}.`); } - return signData(hexAddress, payload, privateKey); + return Promise.resolve(signData(hexAddress, payload, privateKey)); }, submitTx: async (tx: Transaction): Promise => { return await this.provider.submitTx(tx); }, }; + + Freeables.free(...bucket); return this; } From 41c2b0974d2d1d4fd46b48fa721203d956133626 Mon Sep 17 00:00:00 2001 From: yHSJ Date: Wed, 1 Nov 2023 14:47:08 -0400 Subject: [PATCH 11/54] chore: format --- src/lucid/lucid.ts | 8 ++++++-- src/utils/transaction_builder_config.ts | 20 ++++++++++---------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/lucid/lucid.ts b/src/lucid/lucid.ts index 35e6d49c..542dbf34 100644 --- a/src/lucid/lucid.ts +++ b/src/lucid/lucid.ts @@ -69,8 +69,12 @@ export class Lucid { const txBuilderConfig = getTransactionBuilderConfig( protocolParameters, slotConfig, - // deno-lint-ignore no-explicit-any - { url: (provider as any)?.url, projectId: (provider as any)?.projectId } + { + // deno-lint-ignore no-explicit-any + url: (provider as any)?.url, + // deno-lint-ignore no-explicit-any + projectId: (provider as any)?.projectId, + } ); lucid.txBuilderConfig = txBuilderConfig; } diff --git a/src/utils/transaction_builder_config.ts b/src/utils/transaction_builder_config.ts index dd9a5fb4..40d0af65 100644 --- a/src/utils/transaction_builder_config.ts +++ b/src/utils/transaction_builder_config.ts @@ -9,13 +9,13 @@ export function getTransactionBuilderConfig( blockfrostConfig: { url?: string; projectId?: string; - } + }, ) { const bucket: Freeable[] = []; let builderA = C.TransactionBuilderConfigBuilder.new(); const coinsPerUtxoByte = C.BigNum.from_str( - protocolParameters.coinsPerUtxoByte.toString() + protocolParameters.coinsPerUtxoByte.toString(), ); bucket.push(coinsPerUtxoByte); let builderB = builderA.coins_per_utxo_byte(coinsPerUtxoByte); @@ -31,14 +31,14 @@ export function getTransactionBuilderConfig( builderB.free(); const keyDeposit = C.BigNum.from_str( - protocolParameters.keyDeposit.toString() + protocolParameters.keyDeposit.toString(), ); bucket.push(keyDeposit); builderB = builderA.key_deposit(keyDeposit); builderA.free(); const poolDeposit = C.BigNum.from_str( - protocolParameters.poolDeposit.toString() + protocolParameters.poolDeposit.toString(), ); bucket.push(poolDeposit); builderA = builderB.pool_deposit(poolDeposit); @@ -51,21 +51,21 @@ export function getTransactionBuilderConfig( builderB.free(); builderB = builderA.collateral_percentage( - protocolParameters.collateralPercentage + protocolParameters.collateralPercentage, ); builderA.free(); builderA = builderB.max_collateral_inputs( - protocolParameters.maxCollateralInputs + protocolParameters.maxCollateralInputs, ); builderB.free(); const maxTxExMem = C.BigNum.from_str( - protocolParameters.maxTxExMem.toString() + protocolParameters.maxTxExMem.toString(), ); bucket.push(maxTxExMem); const maxTxExSteps = C.BigNum.from_str( - protocolParameters.maxTxExSteps.toString() + protocolParameters.maxTxExSteps.toString(), ); bucket.push(maxTxExSteps); const exUnits = C.ExUnits.new(maxTxExMem, maxTxExSteps); @@ -75,7 +75,7 @@ export function getTransactionBuilderConfig( const exUnitPrices = C.ExUnitPrices.from_float( protocolParameters.priceMem, - protocolParameters.priceStep + protocolParameters.priceStep, ); bucket.push(exUnitPrices); builderA = builderB.ex_unit_prices(exUnitPrices); @@ -90,7 +90,7 @@ export function getTransactionBuilderConfig( const blockfrost = C.Blockfrost.new( blockfrostConfig?.url ?? "" + "utils/tx/evaulate", - blockfrostConfig?.projectId ?? "" + blockfrostConfig?.projectId ?? "", ); bucket.push(blockfrost); builderA = builderB.blockfrost(blockfrost); From 67f6d45571efdfb7da790c58b004bb131f4c8e05 Mon Sep 17 00:00:00 2001 From: yHSJ Date: Wed, 1 Nov 2023 14:58:02 -0400 Subject: [PATCH 12/54] fix: failing tests --- src/lucid/lucid.ts | 66 ++++++++++++++++++++++------------------------ 1 file changed, 32 insertions(+), 34 deletions(-) diff --git a/src/lucid/lucid.ts b/src/lucid/lucid.ts index 542dbf34..79307dd5 100644 --- a/src/lucid/lucid.ts +++ b/src/lucid/lucid.ts @@ -74,7 +74,7 @@ export class Lucid { url: (provider as any)?.url, // deno-lint-ignore no-explicit-any projectId: (provider as any)?.projectId, - } + }, ); lucid.txBuilderConfig = txBuilderConfig; } @@ -117,7 +117,7 @@ export class Lucid { verifyMessage( address: Address | RewardAddress, payload: Payload, - signedMessage: SignedMessage + signedMessage: SignedMessage, ): boolean { const { paymentCredential, @@ -140,7 +140,7 @@ export class Lucid { utxosAtWithUnit( addressOrCredential: Address | Credential, - unit: Unit + unit: Unit, ): Promise { return this.provider.getUtxosWithUnit(addressOrCredential, unit); } @@ -193,13 +193,11 @@ export class Lucid { * Only an Enteprise address (without stake credential) is derived. */ selectWalletFromPrivateKey(privateKey: PrivateKey): Lucid { - const bucket: Freeable[] = []; const priv = C.PrivateKey.from_bech32(privateKey); - bucket.push(priv); const publicKey = priv.to_public(); - bucket.push(publicKey); - const pubKeyHash = priv.to_public().hash(); - bucket.push(pubKeyHash); + priv.free(); + const pubKeyHash = publicKey.hash(); + publicKey.free(); this.wallet = { address: (): Promise
=> { @@ -208,7 +206,7 @@ export class Lucid { bucket.push(stakeCredential); const enterpriseAddress = C.EnterpriseAddress.new( this.network === "Mainnet" ? 1 : 0, - stakeCredential + stakeCredential, ); bucket.push(enterpriseAddress); const address = enterpriseAddress.to_address(); @@ -222,16 +220,18 @@ export class Lucid { rewardAddress: (): Promise => Promise.resolve(null), getUtxos: async (): Promise => { return await this.utxosAt( - paymentCredentialOf(await this.wallet.address()) + paymentCredentialOf(await this.wallet.address()), ); }, getUtxosCore: async (): Promise => { const utxos = await this.utxosAt( - paymentCredentialOf(await this.wallet.address()) + paymentCredentialOf(await this.wallet.address()), ); const coreUtxos = C.TransactionUnspentOutputs.new(); utxos.forEach((utxo) => { - coreUtxos.add(utxoToCore(utxo)); + const coreUtxo = utxoToCore(utxo); + coreUtxos.add(coreUtxo); + coreUtxo.free(); }); return coreUtxos; }, @@ -256,7 +256,7 @@ export class Lucid { }, signMessage: ( address: Address | RewardAddress, - payload: Payload + payload: Payload, ): Promise => { const { paymentCredential, @@ -277,7 +277,6 @@ export class Lucid { }, }; - Freeables.free(...bucket); return this; } @@ -316,7 +315,7 @@ export class Lucid { getUtxos: async (): Promise => { const utxos = ((await api.getUtxos()) || []).map((utxo) => { const parsedUtxo = C.TransactionUnspentOutput.from_bytes( - fromHex(utxo) + fromHex(utxo), ); const finalUtxo = coreToUtxo(parsedUtxo); parsedUtxo.free(); @@ -327,9 +326,9 @@ export class Lucid { getUtxosCore: async (): Promise => { const utxos = C.TransactionUnspentOutputs.new(); ((await api.getUtxos()) || []).forEach((utxo) => { - const cUtxo = C.TransactionUnspentOutput.from_bytes(fromHex(utxo)); - utxos.add(cUtxo); - cUtxo.free(); + const coreUtxo = C.TransactionUnspentOutput.from_bytes(fromHex(utxo)); + utxos.add(coreUtxo); + coreUtxo.free(); }); return utxos; }, @@ -346,7 +345,7 @@ export class Lucid { }, signMessage: async ( address: Address | RewardAddress, - payload: Payload + payload: Payload, ): Promise => { const cAddress = C.Address.from_bech32(address); const hexAddress = toHex(cAddress.to_bytes()); @@ -373,13 +372,13 @@ export class Lucid { if (!rewardAddress && addressDetails.stakeCredential) { if (addressDetails.stakeCredential.type === "Key") { const keyHash = C.Ed25519KeyHash.from_hex( - addressDetails.stakeCredential.hash + addressDetails.stakeCredential.hash, ); const stakeCredential = C.StakeCredential.from_keyhash(keyHash); keyHash.free(); const rewardAddress = C.RewardAddress.new( this.network === "Mainnet" ? 1 : 0, - stakeCredential + stakeCredential, ); stakeCredential.free(); const address = rewardAddress.to_address(); @@ -397,14 +396,13 @@ export class Lucid { }, getUtxosCore: async (): Promise => { const coreUtxos = C.TransactionUnspentOutputs.new(); - (utxos - ? utxos - : await this.utxosAt(paymentCredentialOf(address)) - ).forEach((utxo) => { - const coreUtxo = utxoToCore(utxo); - coreUtxos.add(coreUtxo); - coreUtxo.free(); - }); + (utxos ? utxos : await this.utxosAt(paymentCredentialOf(address))) + .forEach((utxo) => { + const coreUtxo = utxoToCore(utxo); + coreUtxos.add(coreUtxo); + coreUtxo.free(); + }); + return coreUtxos; }, getDelegation: async (): Promise => { @@ -436,7 +434,7 @@ export class Lucid { addressType?: "Base" | "Enterprise"; accountIndex?: number; password?: string; - } + }, ): Lucid { const bucket: Freeable[] = []; const { address, rewardAddress, paymentKey, stakeKey } = walletFromSeed( @@ -446,7 +444,7 @@ export class Lucid { accountIndex: options?.accountIndex || 0, password: options?.password, network: this.network, - } + }, ); const paymentPrivateKey = C.PrivateKey.from_bech32(paymentKey); @@ -486,7 +484,7 @@ export class Lucid { (await this.utxosAt(paymentCredentialOf(address))).forEach((utxo) => { const coreUtxo = utxoToCore(utxo); coreUtxos.add(coreUtxo); - coreUtxos.free(); + coreUtxo.free(); }); return coreUtxos; }, @@ -505,7 +503,7 @@ export class Lucid { const usedKeyHashes = discoverOwnUsedTxKeyHashes( tx, ownKeyHashes, - utxos + utxos, ); const txWitnessSetBuilder = C.TransactionWitnessSetBuilder.new(); @@ -527,7 +525,7 @@ export class Lucid { }, signMessage: ( address: Address | RewardAddress, - payload: Payload + payload: Payload, ): Promise => { const { paymentCredential, From 58f61d21adbe21b7f8d1e98247a41b3cea69c6d1 Mon Sep 17 00:00:00 2001 From: Joaquin Hoyos Date: Wed, 1 Nov 2023 18:30:10 -0300 Subject: [PATCH 13/54] remove CML classes from lucid state, add protocol parameters to constructor --- src/lucid/lucid.ts | 66 +++++++++++++++-------- src/lucid/tx.ts | 57 +++++++------------- tests/mod.test.ts | 129 ++++++++++++++++++++------------------------- 3 files changed, 117 insertions(+), 135 deletions(-) diff --git a/src/lucid/lucid.ts b/src/lucid/lucid.ts index 79307dd5..0788ce68 100644 --- a/src/lucid/lucid.ts +++ b/src/lucid/lucid.ts @@ -20,10 +20,12 @@ import { OutRef, Payload, PrivateKey, + ProtocolParameters, Provider, RewardAddress, SignedMessage, Slot, + SlotConfig, Transaction, TxHash, Unit, @@ -39,22 +41,29 @@ import { Message } from "./message.ts"; import { SLOT_CONFIG_NETWORK } from "../plutus/time.ts"; import { Constr, Data } from "../plutus/data.ts"; import { Emulator } from "../provider/emulator.ts"; -import { getTransactionBuilderConfig } from "../utils/transaction_builder_config.ts"; import { Freeable, Freeables } from "../utils/freeable.ts"; +import { getTransactionBuilderConfig } from "../utils/transaction_builder_config.ts"; export class Lucid { - txBuilderConfig!: C.TransactionBuilderConfig; + protocolParameters?: ProtocolParameters; + slotConfig!: SlotConfig; wallet!: Wallet; provider!: Provider; network: Network = "Mainnet"; utils!: Utils; - static async new(provider?: Provider, network?: Network): Promise { + static async new( + provider?: Provider, + network?: Network, + protocolParameters?: ProtocolParameters, + ): Promise { const lucid = new this(); if (network) lucid.network = network; + if (protocolParameters) { + lucid.protocolParameters = protocolParameters; + } if (provider) { lucid.provider = provider; - const protocolParameters = await provider.getProtocolParameters(); if (lucid.provider instanceof Emulator) { lucid.network = "Custom"; @@ -64,24 +73,35 @@ export class Lucid { slotLength: 1000, }; } - - const slotConfig = SLOT_CONFIG_NETWORK[lucid.network]; - const txBuilderConfig = getTransactionBuilderConfig( - protocolParameters, - slotConfig, - { - // deno-lint-ignore no-explicit-any - url: (provider as any)?.url, - // deno-lint-ignore no-explicit-any - projectId: (provider as any)?.projectId, - }, - ); - lucid.txBuilderConfig = txBuilderConfig; } + if (provider && !lucid.protocolParameters) { + const protocolParameters = await provider.getProtocolParameters(); + lucid.protocolParameters = protocolParameters; + } + lucid.slotConfig = SLOT_CONFIG_NETWORK[lucid.network]; + lucid.utils = new Utils(lucid); return lucid; } + getTransactionBuilderConfig(): C.TransactionBuilderConfig { + if (!this.protocolParameters) { + throw new Error( + "Protocol parameters or slot config not set. Set a provider or iniatilize with protocol parameters.", + ); + } + return getTransactionBuilderConfig( + this.protocolParameters, + this.slotConfig, + { + // deno-lint-ignore no-explicit-any + url: (this.provider as any)?.url, + // deno-lint-ignore no-explicit-any + projectId: (this.provider as any)?.projectId, + }, + ); + } + /** * Switch provider and/or network. * If provider or network unset, no overwriting happens. Provider or network from current instance are taken then. @@ -91,12 +111,16 @@ export class Lucid { throw new Error("Cannot switch when on custom network."); } const lucid = await Lucid.new(provider, network); - this.txBuilderConfig = lucid.txBuilderConfig; + this.protocolParameters = lucid.protocolParameters; + this.slotConfig = lucid.slotConfig; this.provider = provider || this.provider; + // Given that protoclParameters and provider are optional we should fetch protocol parameters if they are not set when switiching providers + if (!this.protocolParameters && provider) { + this.protocolParameters = await provider.getProtocolParameters(); + } this.network = network || this.network; this.wallet = lucid.wallet; - lucid.free(); return this; } @@ -551,8 +575,4 @@ export class Lucid { Freeables.free(...bucket); return this; } - - free() { - this.txBuilderConfig.free(); - } } diff --git a/src/lucid/tx.ts b/src/lucid/tx.ts index 3c1f06b2..9c50157e 100644 --- a/src/lucid/tx.ts +++ b/src/lucid/tx.ts @@ -41,7 +41,9 @@ export class Tx { constructor(lucid: Lucid) { this.lucid = lucid; - this.txBuilder = C.TransactionBuilder.new(this.lucid.txBuilderConfig); + this.txBuilder = C.TransactionBuilder.new( + lucid.getTransactionBuilderConfig(), + ); this.tasks = []; } @@ -216,10 +218,7 @@ export class Tx { this.tasks.push((that) => { const addressDetails = that.lucid.utils.getAddressDetails(rewardAddress); - if ( - addressDetails.type !== "Reward" || - !addressDetails.stakeCredential - ) { + if (addressDetails.type !== "Reward" || !addressDetails.stakeCredential) { throw new Error("Not a reward address provided."); } const credential = addressDetails.stakeCredential.type === "Key" @@ -260,10 +259,7 @@ export class Tx { this.tasks.push((that) => { const addressDetails = that.lucid.utils.getAddressDetails(rewardAddress); - if ( - addressDetails.type !== "Reward" || - !addressDetails.stakeCredential - ) { + if (addressDetails.type !== "Reward" || !addressDetails.stakeCredential) { throw new Error("Not a reward address provided."); } const credential = addressDetails.stakeCredential.type === "Key" @@ -293,10 +289,7 @@ export class Tx { this.tasks.push((that) => { const addressDetails = that.lucid.utils.getAddressDetails(rewardAddress); - if ( - addressDetails.type !== "Reward" || - !addressDetails.stakeCredential - ) { + if (addressDetails.type !== "Reward" || !addressDetails.stakeCredential) { throw new Error("Not a reward address provided."); } const credential = addressDetails.stakeCredential.type === "Key" @@ -337,9 +330,7 @@ export class Tx { that.lucid, ); - const certificate = C.Certificate.new_pool_registration( - poolRegistration, - ); + const certificate = C.Certificate.new_pool_registration(poolRegistration); that.txBuilder.add_certificate(certificate, undefined); }); @@ -357,9 +348,7 @@ export class Tx { // This flag makes sure a pool deposit is not required poolRegistration.set_is_update(true); - const certificate = C.Certificate.new_pool_registration( - poolRegistration, - ); + const certificate = C.Certificate.new_pool_registration(poolRegistration); that.txBuilder.add_certificate(certificate, undefined); }); @@ -412,9 +401,7 @@ export class Tx { addSigner(address: Address | RewardAddress): Tx { const addressDetails = this.lucid.utils.getAddressDetails(address); - if ( - !addressDetails.paymentCredential && !addressDetails.stakeCredential - ) { + if (!addressDetails.paymentCredential && !addressDetails.stakeCredential) { throw new Error("Not a valid address."); } @@ -532,8 +519,7 @@ export class Tx { options?.change?.outputData?.hash, options?.change?.outputData?.asHash, options?.change?.outputData?.inline, - ].filter((b) => b) - .length > 1 + ].filter((b) => b).length > 1 ) { throw new Error( "Not allowed to set hash, asHash and inline at the same time.", @@ -573,9 +559,7 @@ export class Tx { (() => { if (options?.change?.outputData?.hash) { return C.Datum.new_data_hash( - C.DataHash.from_hex( - options.change.outputData.hash, - ), + C.DataHash.from_hex(options.change.outputData.hash), ); } else if (options?.change?.outputData?.asHash) { this.txBuilder.add_plutus_data( @@ -626,7 +610,10 @@ export class Tx { function attachScript( tx: Tx, - { type, script }: + { + type, + script, + }: | SpendingValidator | MintingPolicy | CertificateValidator @@ -661,16 +648,11 @@ async function createPoolRegistration( }); const metadata = poolParams.metadataUrl - ? await fetch( - poolParams.metadataUrl, - ) - .then((res) => res.arrayBuffer()) + ? await fetch(poolParams.metadataUrl).then((res) => res.arrayBuffer()) : null; const metadataHash = metadata - ? C.PoolMetadataHash.from_bytes( - C.hash_blake2b256(new Uint8Array(metadata)), - ) + ? C.PoolMetadataHash.from_bytes(C.hash_blake2b256(new Uint8Array(metadata))) : null; const relays = C.Relays.new(); @@ -727,10 +709,7 @@ async function createPoolRegistration( poolOwners, relays, metadataHash - ? C.PoolMetadata.new( - C.Url.new(poolParams.metadataUrl!), - metadataHash, - ) + ? C.PoolMetadata.new(C.Url.new(poolParams.metadataUrl!), metadataHash) : undefined, ), ); diff --git a/tests/mod.test.ts b/tests/mod.test.ts index cae88b9c..56865760 100644 --- a/tests/mod.test.ts +++ b/tests/mod.test.ts @@ -35,46 +35,8 @@ const lucid = await Lucid.new(undefined, "Preprod"); const slotConfig = SLOT_CONFIG_NETWORK[lucid.network]; const protocolParameters = PROTOCOL_PARAMETERS_DEFAULT; - -lucid.txBuilderConfig = C.TransactionBuilderConfigBuilder.new() - .coins_per_utxo_byte( - C.BigNum.from_str(protocolParameters.coinsPerUtxoByte.toString()), - ) - .fee_algo( - C.LinearFee.new( - C.BigNum.from_str(protocolParameters.minFeeA.toString()), - C.BigNum.from_str(protocolParameters.minFeeB.toString()), - ), - ) - .key_deposit( - C.BigNum.from_str(protocolParameters.keyDeposit.toString()), - ) - .pool_deposit( - C.BigNum.from_str(protocolParameters.poolDeposit.toString()), - ) - .max_tx_size(protocolParameters.maxTxSize) - .max_value_size(protocolParameters.maxValSize) - .collateral_percentage(protocolParameters.collateralPercentage) - .max_collateral_inputs(protocolParameters.maxCollateralInputs) - .max_tx_ex_units( - C.ExUnits.new( - C.BigNum.from_str(protocolParameters.maxTxExMem.toString()), - C.BigNum.from_str(protocolParameters.maxTxExSteps.toString()), - ), - ) - .ex_unit_prices( - C.ExUnitPrices.from_float( - protocolParameters.priceMem, - protocolParameters.priceStep, - ), - ) - .slot_config( - C.BigNum.from_str(slotConfig.zeroTime.toString()), - C.BigNum.from_str(slotConfig.zeroSlot.toString()), - slotConfig.slotLength, - ) - .costmdls(createCostModels(protocolParameters.costModels)) - .build(); +lucid.protocolParameters = protocolParameters; +lucid.slotConfig = slotConfig; lucid.selectWalletFromPrivateKey(privateKey); @@ -415,10 +377,12 @@ Deno.test("toUnit/fromUnit property test", () => { const policyId = toHex(policyRaw); const name = nameRaw.length > 0 ? toHex(nameRaw) : null; const assetName = toLabel(label) + (name || ""); - assertEquals( - fromUnit(toUnit(policyId, name, label)), - { policyId, assetName, name, label }, - ); + assertEquals(fromUnit(toUnit(policyId, name, label)), { + policyId, + assetName, + name, + label, + }); }, ), ); @@ -428,58 +392,77 @@ Deno.test("Preserve task/transaction order", async () => { lucid.selectWalletFrom({ address: "addr_test1qq90qrxyw5qtkex0l7mc86xy9a6xkn5t3fcwm6wq33c38t8nhh356yzp7k3qwmhe4fk0g5u6kx5ka4rz5qcq4j7mvh2sts2cfa", - utxos: [{ - txHash: - "2eefc93bc0dda80e78890f1f965733239e1f64f76555e8dcde1a4aa7db67b129", - outputIndex: 3, - assets: { lovelace: 6770556044n }, - address: - "addr_test1qq90qrxyw5qtkex0l7mc86xy9a6xkn5t3fcwm6wq33c38t8nhh356yzp7k3qwmhe4fk0g5u6kx5ka4rz5qcq4j7mvh2sts2cfa", - datumHash: null, - datum: null, - scriptRef: null, - }], + utxos: [ + { + txHash: + "2eefc93bc0dda80e78890f1f965733239e1f64f76555e8dcde1a4aa7db67b129", + outputIndex: 3, + assets: { lovelace: 6770556044n }, + address: + "addr_test1qq90qrxyw5qtkex0l7mc86xy9a6xkn5t3fcwm6wq33c38t8nhh356yzp7k3qwmhe4fk0g5u6kx5ka4rz5qcq4j7mvh2sts2cfa", + datumHash: null, + datum: null, + scriptRef: null, + }, + ], }); - const txCompA = lucid.newTx().payToAddressWithData( - await lucid.wallet.address(), - { inline: Data.to(0n) }, - {}, - ); + const txCompA = lucid + .newTx() + .payToAddressWithData( + await lucid.wallet.address(), + { inline: Data.to(0n) }, + {}, + ); - const txCompB = lucid.newTx() + const txCompB = lucid + .newTx() .payToAddressWithData( await lucid.wallet.address(), { inline: Data.to(10n) }, {}, ) .compose( - lucid.newTx().payToAddressWithData( - await lucid.wallet.address(), - { inline: Data.to(1n) }, - {}, - ).compose( - lucid.newTx().payToAddressWithData( + lucid + .newTx() + .payToAddressWithData( await lucid.wallet.address(), - { inline: Data.to(2n) }, + { inline: Data.to(1n) }, {}, + ) + .compose( + lucid + .newTx() + .payToAddressWithData( + await lucid.wallet.address(), + { inline: Data.to(2n) }, + {}, + ), ), - ), ); - const tx = await lucid.newTx() + const tx = await lucid + .newTx() .compose(txCompA) .compose(txCompB) .payToAddressWithData( await lucid.wallet.address(), { inline: Data.to(3n) }, {}, - ).complete(); + ) + .complete(); [0n, 10n, 1n, 2n, 3n].forEach((num, i) => { const outputNum = BigInt( - tx.txComplete.body().outputs().get(i).datum()?.as_data()?.get() - .as_integer()?.to_str()!, + tx.txComplete + .body() + .outputs() + .get(i) + .datum() + ?.as_data() + ?.get() + .as_integer() + ?.to_str()!, ); assertEquals(num, outputNum); }); From 5448c26a878ae0108aafc119c8fad19255556cbc Mon Sep 17 00:00:00 2001 From: yHSJ Date: Sat, 4 Nov 2023 13:35:10 -0400 Subject: [PATCH 14/54] feat: manage memory in tx --- src/lucid/tx.ts | 660 +++++++++++++++++++---------------------------- src/utils/cml.ts | 245 ++++++++++++++++++ 2 files changed, 504 insertions(+), 401 deletions(-) create mode 100644 src/utils/cml.ts diff --git a/src/lucid/tx.ts b/src/lucid/tx.ts index 9c50157e..fc468caf 100644 --- a/src/lucid/tx.ts +++ b/src/lucid/tx.ts @@ -21,15 +21,22 @@ import { UTxO, WithdrawalValidator, } from "../types/mod.ts"; +import { + addressFromWithNetworkCheck, + attachScript, + createPoolRegistration, + getDatumFromOutputData, + getScriptWitness, + getStakeCredential, +} from "../utils/cml.ts"; +import { type FreeableBucket, Freeables } from "../utils/freeable.ts"; import { assetsToValue, fromHex, - networkToId, toHex, toScriptRef, utxoToCore, } from "../utils/mod.ts"; -import { applyDoubleCborEncoding } from "../utils/utils.ts"; import { Lucid } from "./lucid.ts"; import { TxComplete } from "./tx_complete.ts"; @@ -42,7 +49,7 @@ export class Tx { constructor(lucid: Lucid) { this.lucid = lucid; this.txBuilder = C.TransactionBuilder.new( - lucid.getTransactionBuilderConfig(), + lucid.getTransactionBuilderConfig() ); this.tasks = []; } @@ -50,15 +57,22 @@ export class Tx { /** Read data from utxos. These utxos are only referenced and not spent. */ readFrom(utxos: UTxO[]): Tx { this.tasks.push(async (that) => { - for (const utxo of utxos) { - if (utxo.datumHash) { - utxo.datum = Data.to(await that.lucid.datumOf(utxo)); - // Add datum to witness set, so it can be read from validators - const plutusData = C.PlutusData.from_bytes(fromHex(utxo.datum!)); - that.txBuilder.add_plutus_data(plutusData); + const bucket: FreeableBucket = []; + try { + for (const utxo of utxos) { + if (utxo.datumHash) { + utxo.datum = Data.to(await that.lucid.datumOf(utxo)); + // Add datum to witness set, so it can be read from validators + const plutusData = C.PlutusData.from_bytes(fromHex(utxo.datum!)); + bucket.push(plutusData); + that.txBuilder.add_plutus_data(plutusData); + } + const coreUtxo = utxoToCore(utxo); + bucket.push(coreUtxo); + that.txBuilder.add_reference_input(coreUtxo); } - const coreUtxo = utxoToCore(utxo); - that.txBuilder.add_reference_input(coreUtxo); + } finally { + Freeables.free(...bucket); } }); return this; @@ -70,24 +84,26 @@ export class Tx { */ collectFrom(utxos: UTxO[], redeemer?: Redeemer): Tx { this.tasks.push(async (that) => { - for (const utxo of utxos) { - if (utxo.datumHash && !utxo.datum) { - utxo.datum = Data.to(await that.lucid.datumOf(utxo)); + const bucket: FreeableBucket = []; + try { + for (const utxo of utxos) { + if (utxo.datumHash && !utxo.datum) { + utxo.datum = Data.to(await that.lucid.datumOf(utxo)); + } + const coreUtxo = utxoToCore(utxo); + bucket.push(coreUtxo); + // We don't free Options as the ownership is passed to the txBuilder + const scriptWitness = redeemer + ? getScriptWitness( + redeemer, + utxo.datumHash && utxo.datum ? utxo.datum : undefined + ) + : undefined; + + that.txBuilder.add_input(coreUtxo, scriptWitness); } - const coreUtxo = utxoToCore(utxo); - that.txBuilder.add_input( - coreUtxo, - (redeemer as undefined) && - C.ScriptWitness.new_plutus_witness( - C.PlutusWitness.new( - C.PlutusData.from_bytes(fromHex(redeemer!)), - utxo.datumHash && utxo.datum - ? C.PlutusData.from_bytes(fromHex(utxo.datum!)) - : undefined, - undefined, - ), - ), - ); + } finally { + Freeables.free(...bucket); } }); return this; @@ -100,34 +116,32 @@ export class Tx { */ mintAssets(assets: Assets, redeemer?: Redeemer): Tx { this.tasks.push((that) => { - const units = Object.keys(assets); - const policyId = units[0].slice(0, 56); - const mintAssets = C.MintAssets.new(); - units.forEach((unit) => { - if (unit.slice(0, 56) !== policyId) { - throw new Error( - "Only one policy id allowed. You can chain multiple mintAssets functions together if you need to mint assets with different policy ids.", - ); - } - mintAssets.insert( - C.AssetName.new(fromHex(unit.slice(56))), - C.Int.from_str(assets[unit].toString()), - ); - }); - const scriptHash = C.ScriptHash.from_bytes(fromHex(policyId)); - that.txBuilder.add_mint( - scriptHash, - mintAssets, - redeemer - ? C.ScriptWitness.new_plutus_witness( - C.PlutusWitness.new( - C.PlutusData.from_bytes(fromHex(redeemer!)), - undefined, - undefined, - ), - ) - : undefined, - ); + const bucket: FreeableBucket = []; + try { + const units = Object.keys(assets); + const policyId = units[0].slice(0, 56); + const mintAssets = C.MintAssets.new(); + bucket.push(mintAssets); + units.forEach((unit) => { + if (unit.slice(0, 56) !== policyId) { + throw new Error( + "Only one policy id allowed. You can chain multiple mintAssets functions together if you need to mint assets with different policy ids." + ); + } + const assetName = C.AssetName.new(fromHex(unit.slice(56))); + const int = C.Int.from_str(assets[unit].toString()); + // Int is being passed by value so we don't need to free it + bucket.push(assetName); + mintAssets.insert(assetName, int); + }); + const scriptHash = C.ScriptHash.from_bytes(fromHex(policyId)); + // We don't free Options as the ownership is passed to the txBuilder + const scriptWitness = redeemer ? getScriptWitness(redeemer) : undefined; + bucket.push(scriptHash); + that.txBuilder.add_mint(scriptHash, mintAssets, scriptWitness); + } finally { + Freeables.free(...bucket); + } }); return this; } @@ -135,11 +149,12 @@ export class Tx { /** Pay to a public key or native script address. */ payToAddress(address: Address, assets: Assets): Tx { this.tasks.push((that) => { - const output = C.TransactionOutput.new( - addressFromWithNetworkCheck(address, that.lucid), - assetsToValue(assets), - ); + const addr = addressFromWithNetworkCheck(address, that.lucid); + const value = assetsToValue(assets); + + const output = C.TransactionOutput.new(addr, value); that.txBuilder.add_output(output); + Freeables.free(output, addr, value); }); return this; } @@ -148,45 +163,64 @@ export class Tx { payToAddressWithData( address: Address, outputData: Datum | OutputData, - assets: Assets, + assets: Assets ): Tx { this.tasks.push((that) => { - if (typeof outputData === "string") { - outputData = { asHash: outputData }; - } - - if ( - [outputData.hash, outputData.asHash, outputData.inline].filter((b) => b) - .length > 1 - ) { - throw new Error( - "Not allowed to set hash, asHash and inline at the same time.", - ); - } + const bucket: FreeableBucket = []; + try { + if (typeof outputData === "string") { + outputData = { asHash: outputData }; + } - const output = C.TransactionOutput.new( - addressFromWithNetworkCheck(address, that.lucid), - assetsToValue(assets), - ); + if ( + [outputData.hash, outputData.asHash, outputData.inline].filter( + (b) => b + ).length > 1 + ) { + throw new Error( + "Not allowed to set hash, asHash and inline at the same time." + ); + } - if (outputData.hash) { - output.set_datum( - C.Datum.new_data_hash(C.DataHash.from_hex(outputData.hash)), - ); - } else if (outputData.asHash) { - const plutusData = C.PlutusData.from_bytes(fromHex(outputData.asHash)); - output.set_datum(C.Datum.new_data_hash(C.hash_plutus_data(plutusData))); - that.txBuilder.add_plutus_data(plutusData); - } else if (outputData.inline) { - const plutusData = C.PlutusData.from_bytes(fromHex(outputData.inline)); - output.set_datum(C.Datum.new_data(C.Data.new(plutusData))); - } + const addr = addressFromWithNetworkCheck(address, that.lucid); + const value = assetsToValue(assets); + const output = C.TransactionOutput.new(addr, value); + bucket.push(output, addr, value); + + if (outputData.hash) { + const dataHash = C.DataHash.from_hex(outputData.hash); + const datum = C.Datum.new_data_hash(dataHash); + bucket.push(dataHash, datum); + output.set_datum(datum); + } else if (outputData.asHash) { + const plutusData = C.PlutusData.from_bytes( + fromHex(outputData.asHash) + ); + const dataHash = C.hash_plutus_data(plutusData); + const datum = C.Datum.new_data_hash(dataHash); + bucket.push(plutusData, dataHash, datum); + output.set_datum(datum); + that.txBuilder.add_plutus_data(plutusData); + } else if (outputData.inline) { + const plutusData = C.PlutusData.from_bytes( + fromHex(outputData.inline) + ); + const data = C.Data.new(plutusData); + const datum = C.Datum.new_data(data); + bucket.push(plutusData, data, datum); + output.set_datum(datum); + } - const script = outputData.scriptRef; - if (script) { - output.set_script_ref(toScriptRef(script)); + const script = outputData.scriptRef; + if (script) { + const scriptRef = toScriptRef(script); + bucket.push(scriptRef); + output.set_script_ref(toScriptRef(script)); + } + that.txBuilder.add_output(output); + } finally { + Freeables.free(...bucket); } - that.txBuilder.add_output(output); }); return this; } @@ -195,7 +229,7 @@ export class Tx { payToContract( address: Address, outputData: Datum | OutputData, - assets: Assets, + assets: Assets ): Tx { if (typeof outputData === "string") { outputData = { asHash: outputData }; @@ -203,7 +237,7 @@ export class Tx { if (!(outputData.hash || outputData.asHash || outputData.inline)) { throw new Error( - "No datum set. Script output becomes unspendable without datum.", + "No datum set. Script output becomes unspendable without datum." ); } return this.payToAddressWithData(address, outputData, assets); @@ -213,7 +247,7 @@ export class Tx { delegateTo( rewardAddress: RewardAddress, poolId: PoolId, - redeemer?: Redeemer, + redeemer?: Redeemer ): Tx { this.tasks.push((that) => { const addressDetails = that.lucid.utils.getAddressDetails(rewardAddress); @@ -221,35 +255,18 @@ export class Tx { if (addressDetails.type !== "Reward" || !addressDetails.stakeCredential) { throw new Error("Not a reward address provided."); } - const credential = addressDetails.stakeCredential.type === "Key" - ? C.StakeCredential.from_keyhash( - C.Ed25519KeyHash.from_bytes( - fromHex(addressDetails.stakeCredential.hash), - ), - ) - : C.StakeCredential.from_scripthash( - C.ScriptHash.from_bytes( - fromHex(addressDetails.stakeCredential.hash), - ), - ); - - that.txBuilder.add_certificate( - C.Certificate.new_stake_delegation( - C.StakeDelegation.new( - credential, - C.Ed25519KeyHash.from_bech32(poolId), - ), - ), - redeemer - ? C.ScriptWitness.new_plutus_witness( - C.PlutusWitness.new( - C.PlutusData.from_bytes(fromHex(redeemer!)), - undefined, - undefined, - ), - ) - : undefined, + const credential = getStakeCredential( + addressDetails.stakeCredential.hash, + addressDetails.stakeCredential.type ); + + const keyHash = C.Ed25519KeyHash.from_bech32(poolId); + const delegation = C.StakeDelegation.new(credential, keyHash); + // We don't free Options as the ownership is passed to the txBuilder + const scriptWitness = redeemer ? getScriptWitness(redeemer) : undefined; + const certificate = C.Certificate.new_stake_delegation(delegation); + that.txBuilder.add_certificate(certificate, scriptWitness); + Freeables.free(keyHash, delegation, credential, certificate); }); return this; } @@ -262,24 +279,16 @@ export class Tx { if (addressDetails.type !== "Reward" || !addressDetails.stakeCredential) { throw new Error("Not a reward address provided."); } - const credential = addressDetails.stakeCredential.type === "Key" - ? C.StakeCredential.from_keyhash( - C.Ed25519KeyHash.from_bytes( - fromHex(addressDetails.stakeCredential.hash), - ), - ) - : C.StakeCredential.from_scripthash( - C.ScriptHash.from_bytes( - fromHex(addressDetails.stakeCredential.hash), - ), - ); - - that.txBuilder.add_certificate( - C.Certificate.new_stake_registration( - C.StakeRegistration.new(credential), - ), - undefined, + const credential = getStakeCredential( + addressDetails.stakeCredential.hash, + addressDetails.stakeCredential.type ); + const stakeRegistration = C.StakeRegistration.new(credential); + const certificate = + C.Certificate.new_stake_registration(stakeRegistration); + + that.txBuilder.add_certificate(certificate, undefined); + Freeables.free(credential, stakeRegistration, certificate); }); return this; } @@ -292,32 +301,18 @@ export class Tx { if (addressDetails.type !== "Reward" || !addressDetails.stakeCredential) { throw new Error("Not a reward address provided."); } - const credential = addressDetails.stakeCredential.type === "Key" - ? C.StakeCredential.from_keyhash( - C.Ed25519KeyHash.from_bytes( - fromHex(addressDetails.stakeCredential.hash), - ), - ) - : C.StakeCredential.from_scripthash( - C.ScriptHash.from_bytes( - fromHex(addressDetails.stakeCredential.hash), - ), - ); - - that.txBuilder.add_certificate( - C.Certificate.new_stake_deregistration( - C.StakeDeregistration.new(credential), - ), - redeemer - ? C.ScriptWitness.new_plutus_witness( - C.PlutusWitness.new( - C.PlutusData.from_bytes(fromHex(redeemer!)), - undefined, - undefined, - ), - ) - : undefined, + const credential = getStakeCredential( + addressDetails.stakeCredential.hash, + addressDetails.stakeCredential.type ); + const stakeDeregistration = C.StakeDeregistration.new(credential); + const certificate = + C.Certificate.new_stake_deregistration(stakeDeregistration); + // We don't free Options as the ownership is passed to the txBuilder + const scriptWitness = redeemer ? getScriptWitness(redeemer) : undefined; + + that.txBuilder.add_certificate(certificate, scriptWitness); + Freeables.free(credential, stakeDeregistration, certificate); }); return this; } @@ -327,12 +322,13 @@ export class Tx { this.tasks.push(async (that) => { const poolRegistration = await createPoolRegistration( poolParams, - that.lucid, + that.lucid ); const certificate = C.Certificate.new_pool_registration(poolRegistration); that.txBuilder.add_certificate(certificate, undefined); + Freeables.free(certificate, poolRegistration); }); return this; } @@ -342,13 +338,14 @@ export class Tx { this.tasks.push(async (that) => { const poolRegistration = await createPoolRegistration( poolParams, - that.lucid, + that.lucid ); // This flag makes sure a pool deposit is not required poolRegistration.set_is_update(true); const certificate = C.Certificate.new_pool_registration(poolRegistration); + Freeables.free(poolRegistration, certificate); that.txBuilder.add_certificate(certificate, undefined); }); @@ -360,10 +357,11 @@ export class Tx { */ retirePool(poolId: PoolId, epoch: number): Tx { this.tasks.push((that) => { - const certificate = C.Certificate.new_pool_retirement( - C.PoolRetirement.new(C.Ed25519KeyHash.from_bech32(poolId), epoch), - ); + const keyHash = C.Ed25519KeyHash.from_bech32(poolId); + const poolRetirement = C.PoolRetirement.new(keyHash, epoch); + const certificate = C.Certificate.new_pool_retirement(poolRetirement); that.txBuilder.add_certificate(certificate, undefined); + Freeables.free(keyHash, poolRetirement, certificate); }); return this; } @@ -371,24 +369,15 @@ export class Tx { withdraw( rewardAddress: RewardAddress, amount: Lovelace, - redeemer?: Redeemer, + redeemer?: Redeemer ): Tx { this.tasks.push((that) => { - that.txBuilder.add_withdrawal( - C.RewardAddress.from_address( - addressFromWithNetworkCheck(rewardAddress, that.lucid), - )!, - C.BigNum.from_str(amount.toString()), - redeemer - ? C.ScriptWitness.new_plutus_witness( - C.PlutusWitness.new( - C.PlutusData.from_bytes(fromHex(redeemer!)), - undefined, - undefined, - ), - ) - : undefined, - ); + const addr = addressFromWithNetworkCheck(rewardAddress, that.lucid); + const rewardAddr = C.RewardAddress.from_address(addr)!; + const amountBigNum = C.BigNum.from_str(amount.toString()); + const scriptWitness = redeemer ? getScriptWitness(redeemer) : undefined; + that.txBuilder.add_withdrawal(rewardAddr, amountBigNum, scriptWitness); + Freeables.free(addr, rewardAddr, amountBigNum, scriptWitness); }); return this; } @@ -405,9 +394,10 @@ export class Tx { throw new Error("Not a valid address."); } - const credential = addressDetails.type === "Reward" - ? addressDetails.stakeCredential! - : addressDetails.paymentCredential!; + const credential = + addressDetails.type === "Reward" + ? addressDetails.stakeCredential! + : addressDetails.paymentCredential!; if (credential.type === "Script") { throw new Error("Only key hashes are allowed as signers."); @@ -418,9 +408,9 @@ export class Tx { /** Add a payment or stake key hash as a required signer of the transaction. */ addSignerKey(keyHash: PaymentKeyHash | StakeKeyHash): Tx { this.tasks.push((that) => { - that.txBuilder.add_required_signer( - C.Ed25519KeyHash.from_bytes(fromHex(keyHash)), - ); + const key = C.Ed25519KeyHash.from_bytes(fromHex(keyHash)); + that.txBuilder.add_required_signer(key); + Freeables.free(key); }); return this; } @@ -428,9 +418,9 @@ export class Tx { validFrom(unixTime: UnixTime): Tx { this.tasks.push((that) => { const slot = that.lucid.utils.unixTimeToSlot(unixTime); - that.txBuilder.set_validity_start_interval( - C.BigNum.from_str(slot.toString()), - ); + const slotNum = C.BigNum.from_str(slot.toString()); + that.txBuilder.set_validity_start_interval(slotNum); + Freeables.free(slotNum); }); return this; } @@ -438,17 +428,18 @@ export class Tx { validTo(unixTime: UnixTime): Tx { this.tasks.push((that) => { const slot = that.lucid.utils.unixTimeToSlot(unixTime); - that.txBuilder.set_ttl(C.BigNum.from_str(slot.toString())); + const slotNum = C.BigNum.from_str(slot.toString()); + that.txBuilder.set_ttl(slotNum); + Freeables.free(slotNum); }); return this; } attachMetadata(label: Label, metadata: Json): Tx { this.tasks.push((that) => { - that.txBuilder.add_json_metadatum( - C.BigNum.from_str(label.toString()), - JSON.stringify(metadata), - ); + const labelNum = C.BigNum.from_str(label.toString()); + that.txBuilder.add_json_metadatum(labelNum, JSON.stringify(metadata)); + Freeables.free(labelNum); }); return this; } @@ -456,11 +447,13 @@ export class Tx { /** Converts strings to bytes if prefixed with **'0x'**. */ attachMetadataWithConversion(label: Label, metadata: Json): Tx { this.tasks.push((that) => { + const labelNum = C.BigNum.from_str(label.toString()); that.txBuilder.add_json_metadatum_with_schema( - C.BigNum.from_str(label.toString()), + labelNum, JSON.stringify(metadata), - C.MetadataJsonSchema.BasicConversions, + C.MetadataJsonSchema.BasicConversions ); + Freeables.free(labelNum); }); return this; } @@ -468,9 +461,11 @@ export class Tx { /** Explicitely set the network id in the transaction body. */ addNetworkId(id: number): Tx { this.tasks.push((that) => { - that.txBuilder.set_network_id( - C.NetworkId.from_bytes(fromHex(id.toString(16).padStart(2, "0"))), + const networkId = C.NetworkId.from_bytes( + fromHex(id.toString(16).padStart(2, "0")) ); + that.txBuilder.set_network_id(networkId); + Freeables.free(networkId); }); return this; } @@ -509,91 +504,78 @@ export class Tx { return this; } + free() { + this.txBuilder.free(); + } + + /** Completes the transaction. This might fail, you should free the txBuilder when you are done with it. */ async complete(options?: { change?: { address?: Address; outputData?: OutputData }; coinSelection?: boolean; nativeUplc?: boolean; }): Promise { - if ( - [ - options?.change?.outputData?.hash, - options?.change?.outputData?.asHash, - options?.change?.outputData?.inline, - ].filter((b) => b).length > 1 - ) { - throw new Error( - "Not allowed to set hash, asHash and inline at the same time.", - ); - } - - let task = this.tasks.shift(); - while (task) { - await task(this); - task = this.tasks.shift(); - } + const bucket: FreeableBucket = []; + try { + if ( + [ + options?.change?.outputData?.hash, + options?.change?.outputData?.asHash, + options?.change?.outputData?.inline, + ].filter((b) => b).length > 1 + ) { + throw new Error( + "Not allowed to set hash, asHash and inline at the same time." + ); + } - const utxos = await this.lucid.wallet.getUtxosCore(); + let task = this.tasks.shift(); + while (task) { + await task(this); + task = this.tasks.shift(); + } - const changeAddress: C.Address = addressFromWithNetworkCheck( - options?.change?.address || (await this.lucid.wallet.address()), - this.lucid, - ); + const utxos = await this.lucid.wallet.getUtxosCore(); - if (options?.coinSelection || options?.coinSelection === undefined) { - this.txBuilder.add_inputs_from( - utxos, - changeAddress, - Uint32Array.from([ - 200, // weight ideal > 100 inputs - 1000, // weight ideal < 100 inputs - 1500, // weight assets if plutus - 800, // weight assets if not plutus - 800, // weight distance if not plutus - 5000, // weight utxos - ]), + const changeAddress: C.Address = addressFromWithNetworkCheck( + options?.change?.address || (await this.lucid.wallet.address()), + this.lucid ); - } + bucket.push(utxos, changeAddress); + + if (options?.coinSelection || options?.coinSelection === undefined) { + this.txBuilder.add_inputs_from( + utxos, + changeAddress, + Uint32Array.from([ + 200, // weight ideal > 100 inputs + 1000, // weight ideal < 100 inputs + 1500, // weight assets if plutus + 800, // weight assets if not plutus + 800, // weight distance if not plutus + 5000, // weight utxos + ]) + ); + } - this.txBuilder.balance( - changeAddress, - (() => { - if (options?.change?.outputData?.hash) { - return C.Datum.new_data_hash( - C.DataHash.from_hex(options.change.outputData.hash), - ); - } else if (options?.change?.outputData?.asHash) { - this.txBuilder.add_plutus_data( - C.PlutusData.from_bytes(fromHex(options.change.outputData.asHash)), - ); - return C.Datum.new_data_hash( - C.hash_plutus_data( - C.PlutusData.from_bytes( - fromHex(options.change.outputData.asHash), - ), - ), - ); - } else if (options?.change?.outputData?.inline) { - return C.Datum.new_data( - C.Data.new( - C.PlutusData.from_bytes( - fromHex(options.change.outputData.inline), - ), - ), - ); - } else { - return undefined; - } - })(), - ); + const { datum, plutusData } = getDatumFromOutputData( + options?.change?.outputData + ); + if (plutusData) { + this.txBuilder.add_plutus_data(plutusData); + } + bucket.push(datum, plutusData); + this.txBuilder.balance(changeAddress, datum); - return new TxComplete( - this.lucid, - await this.txBuilder.construct( + const tx = await this.txBuilder.construct( utxos, changeAddress, - options?.nativeUplc === undefined ? true : options?.nativeUplc, - ), - ); + options?.nativeUplc === undefined ? true : options?.nativeUplc + ); + + return new TxComplete(this.lucid, tx); + } finally { + Freeables.free(...bucket); + } } /** Return the current transaction body in Hex encoded Cbor. */ @@ -607,127 +589,3 @@ export class Tx { return toHex(this.txBuilder.to_bytes()); } } - -function attachScript( - tx: Tx, - { - type, - script, - }: - | SpendingValidator - | MintingPolicy - | CertificateValidator - | WithdrawalValidator, -) { - if (type === "Native") { - return tx.txBuilder.add_native_script( - C.NativeScript.from_bytes(fromHex(script)), - ); - } else if (type === "PlutusV1") { - return tx.txBuilder.add_plutus_script( - C.PlutusScript.from_bytes(fromHex(applyDoubleCborEncoding(script))), - ); - } else if (type === "PlutusV2") { - return tx.txBuilder.add_plutus_v2_script( - C.PlutusScript.from_bytes(fromHex(applyDoubleCborEncoding(script))), - ); - } - throw new Error("No variant matched."); -} - -async function createPoolRegistration( - poolParams: PoolParams, - lucid: Lucid, -): Promise { - const poolOwners = C.Ed25519KeyHashes.new(); - poolParams.owners.forEach((owner) => { - const { stakeCredential } = lucid.utils.getAddressDetails(owner); - if (stakeCredential?.type === "Key") { - poolOwners.add(C.Ed25519KeyHash.from_hex(stakeCredential.hash)); - } else throw new Error("Only key hashes allowed for pool owners."); - }); - - const metadata = poolParams.metadataUrl - ? await fetch(poolParams.metadataUrl).then((res) => res.arrayBuffer()) - : null; - - const metadataHash = metadata - ? C.PoolMetadataHash.from_bytes(C.hash_blake2b256(new Uint8Array(metadata))) - : null; - - const relays = C.Relays.new(); - poolParams.relays.forEach((relay) => { - switch (relay.type) { - case "SingleHostIp": { - const ipV4 = relay.ipV4 - ? C.Ipv4.new( - new Uint8Array(relay.ipV4.split(".").map((b) => parseInt(b))), - ) - : undefined; - const ipV6 = relay.ipV6 - ? C.Ipv6.new(fromHex(relay.ipV6.replaceAll(":", ""))) - : undefined; - relays.add( - C.Relay.new_single_host_addr( - C.SingleHostAddr.new(relay.port, ipV4, ipV6), - ), - ); - break; - } - case "SingleHostDomainName": { - relays.add( - C.Relay.new_single_host_name( - C.SingleHostName.new( - relay.port, - C.DNSRecordAorAAAA.new(relay.domainName!), - ), - ), - ); - break; - } - case "MultiHost": { - relays.add( - C.Relay.new_multi_host_name( - C.MultiHostName.new(C.DNSRecordSRV.new(relay.domainName!)), - ), - ); - break; - } - } - }); - - return C.PoolRegistration.new( - C.PoolParams.new( - C.Ed25519KeyHash.from_bech32(poolParams.poolId), - C.VRFKeyHash.from_hex(poolParams.vrfKeyHash), - C.BigNum.from_str(poolParams.pledge.toString()), - C.BigNum.from_str(poolParams.cost.toString()), - C.UnitInterval.from_float(poolParams.margin), - C.RewardAddress.from_address( - addressFromWithNetworkCheck(poolParams.rewardAddress, lucid), - )!, - poolOwners, - relays, - metadataHash - ? C.PoolMetadata.new(C.Url.new(poolParams.metadataUrl!), metadataHash) - : undefined, - ), - ); -} - -function addressFromWithNetworkCheck( - address: Address | RewardAddress, - lucid: Lucid, -): C.Address { - const { type, networkId } = lucid.utils.getAddressDetails(address); - - const actualNetworkId = networkToId(lucid.network); - if (networkId !== actualNetworkId) { - throw new Error( - `Invalid address: Expected address with network id ${actualNetworkId}, but got ${networkId}`, - ); - } - return type === "Byron" - ? C.ByronAddress.from_base58(address).to_address() - : C.Address.from_bech32(address); -} diff --git a/src/utils/cml.ts b/src/utils/cml.ts new file mode 100644 index 00000000..755a4503 --- /dev/null +++ b/src/utils/cml.ts @@ -0,0 +1,245 @@ +import { + Address, + C, + CertificateValidator, + Datum, + Lucid, + MintingPolicy, + OutputData, + PoolParams, + Redeemer, + RewardAddress, + SpendingValidator, + Tx, + WithdrawalValidator, + applyDoubleCborEncoding, + fromHex, + networkToId, +} from "../mod.ts"; +import { FreeableBucket, Freeables } from "./freeable.ts"; + +export function getScriptWitness( + redeemer: Redeemer, + datum?: Datum +): C.ScriptWitness { + const bucket: FreeableBucket = []; + try { + const plutusRedeemer = C.PlutusData.from_bytes(fromHex(redeemer!)); + const plutusData = datum + ? C.PlutusData.from_bytes(fromHex(datum)) + : undefined; + const plutusWitness = C.PlutusWitness.new( + plutusRedeemer, + plutusData, + undefined + ); + // We shouldn't free plutusData as it is an Option + bucket.push(plutusRedeemer, plutusWitness); + return C.ScriptWitness.new_plutus_witness(plutusWitness); + } finally { + Freeables.free(...bucket); + } +} + +export function getStakeCredential(hash: string, type: "Key" | "Script") { + if (type === "Key") { + const keyHash = C.Ed25519KeyHash.from_bytes(fromHex(hash)); + const credential = C.StakeCredential.from_keyhash(keyHash); + Freeables.free(keyHash); + return credential; + } + const scriptHash = C.ScriptHash.from_bytes(fromHex(hash)); + const credential = C.StakeCredential.from_scripthash(scriptHash); + Freeables.free(scriptHash); + return credential; +} + +export async function createPoolRegistration( + poolParams: PoolParams, + lucid: Lucid +): Promise { + const bucket: FreeableBucket = []; + try { + const poolOwners = C.Ed25519KeyHashes.new(); + bucket.push(poolOwners); + poolParams.owners.forEach((owner) => { + const { stakeCredential } = lucid.utils.getAddressDetails(owner); + if (stakeCredential?.type === "Key") { + const keyHash = C.Ed25519KeyHash.from_hex(stakeCredential.hash); + poolOwners.add(keyHash); + bucket.push(keyHash); + } else throw new Error("Only key hashes allowed for pool owners."); + }); + + const metadata = poolParams.metadataUrl + ? await fetch(poolParams.metadataUrl).then((res) => res.arrayBuffer()) + : null; + + const metadataHash = metadata + ? C.PoolMetadataHash.from_bytes( + C.hash_blake2b256(new Uint8Array(metadata)) + ) + : null; + + const relays = C.Relays.new(); + bucket.push(metadataHash, relays); + poolParams.relays.forEach((relay) => { + switch (relay.type) { + case "SingleHostIp": { + const ipV4 = relay.ipV4 + ? C.Ipv4.new( + new Uint8Array(relay.ipV4.split(".").map((b) => parseInt(b))) + ) + : undefined; + const ipV6 = relay.ipV6 + ? C.Ipv6.new(fromHex(relay.ipV6.replaceAll(":", ""))) + : undefined; + const host = C.SingleHostAddr.new(relay.port, ipV4, ipV6); + const newRelay = C.Relay.new_single_host_addr(host); + //We shouldn't free ipV4 and ipV6 as they are optionals + bucket.push(host, newRelay); + relays.add(newRelay); + break; + } + case "SingleHostDomainName": { + const record = C.DNSRecordAorAAAA.new(relay.domainName!); + const host = C.SingleHostName.new(relay.port, record); + const newRelay = C.Relay.new_single_host_name(host); + bucket.push(record, host, newRelay); + relays.add(newRelay); + break; + } + case "MultiHost": { + const record = C.DNSRecordSRV.new(relay.domainName!); + const host = C.MultiHostName.new(record); + const newRelay = C.Relay.new_multi_host_name(host); + bucket.push(record, host, newRelay); + relays.add(newRelay); + break; + } + } + }); + + const operator = C.Ed25519KeyHash.from_bech32(poolParams.poolId); + const vrfKeyHash = C.VRFKeyHash.from_hex(poolParams.vrfKeyHash); + const pledge = C.BigNum.from_str(poolParams.pledge.toString()); + const cost = C.BigNum.from_str(poolParams.cost.toString()); + + const margin = C.UnitInterval.from_float(poolParams.margin); + const addr = addressFromWithNetworkCheck(poolParams.rewardAddress, lucid); + const rewardAddress = C.RewardAddress.from_address(addr); + const url = C.Url.new(poolParams.metadataUrl!); + const poolMetadata = metadataHash + ? C.PoolMetadata.new(url, metadataHash) + : undefined; + bucket.push( + operator, + vrfKeyHash, + pledge, + cost, + margin, + addr, + rewardAddress, + url, + poolMetadata + ); + + const params = C.PoolParams.new( + operator, + vrfKeyHash, + pledge, + cost, + margin, + rewardAddress!, + poolOwners, + relays, + poolMetadata + ); + + const poolRegistration = C.PoolRegistration.new(params); + return poolRegistration; + } finally { + Freeables.free(...bucket); + } +} + +export function attachScript( + tx: Tx, + { + type, + script, + }: + | SpendingValidator + | MintingPolicy + | CertificateValidator + | WithdrawalValidator +) { + if (type === "Native") { + const nativeScript = C.NativeScript.from_bytes(fromHex(script)); + tx.txBuilder.add_native_script(nativeScript); + Freeables.free(nativeScript); + return; + } else if (type === "PlutusV1") { + const plutusScript = C.PlutusScript.from_bytes( + fromHex(applyDoubleCborEncoding(script)) + ); + tx.txBuilder.add_plutus_script(plutusScript); + Freeables.free(plutusScript); + return; + } else if (type === "PlutusV2") { + const plutusScript = C.PlutusScript.from_bytes( + fromHex(applyDoubleCborEncoding(script)) + ); + tx.txBuilder.add_plutus_v2_script(plutusScript); + Freeables.free(plutusScript); + return; + } + throw new Error("No variant matched."); +} + +export function addressFromWithNetworkCheck( + address: Address | RewardAddress, + lucid: Lucid +): C.Address { + const { type, networkId } = lucid.utils.getAddressDetails(address); + + const actualNetworkId = networkToId(lucid.network); + if (networkId !== actualNetworkId) { + throw new Error( + `Invalid address: Expected address with network id ${actualNetworkId}, but got ${networkId}` + ); + } + if (type === "Byron") { + const byron = C.ByronAddress.from_base58(address); + const addr = byron.to_address(); + byron.free(); + return addr; + } + return C.Address.from_bech32(address); +} + +export function getDatumFromOutputData(outputData?: OutputData): { + datum?: C.Datum | undefined; + plutusData?: C.PlutusData; +} { + if (outputData?.hash) { + const hash = C.DataHash.from_hex(outputData.hash); + const datum = C.Datum.new_data_hash(hash); + hash.free(); + return { datum }; + } else if (outputData?.asHash) { + const plutusData = C.PlutusData.from_bytes(fromHex(outputData.asHash)); + const dataHash = C.hash_plutus_data(plutusData); + const datum = C.Datum.new_data_hash(dataHash); + dataHash.free(); + return { plutusData, datum }; + } else if (outputData?.inline) { + const plutusData = C.PlutusData.from_bytes(fromHex(outputData.inline)); + const data = C.Data.new(plutusData); + const datum = C.Datum.new_data(data); + Freeables.free(plutusData, data); + return { datum }; + } else { + return {}; + } +} From 37d49532c8a9f0b1dee572fa078c9a6d9fe40637 Mon Sep 17 00:00:00 2001 From: yHSJ Date: Sat, 4 Nov 2023 14:09:41 -0400 Subject: [PATCH 15/54] fix: don't free null pointers --- src/lucid/tx.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lucid/tx.ts b/src/lucid/tx.ts index fc468caf..e5d58286 100644 --- a/src/lucid/tx.ts +++ b/src/lucid/tx.ts @@ -534,13 +534,14 @@ export class Tx { task = this.tasks.shift(); } + // We don't free `utxos` as it is passed as an Option to the txBuilder and the ownership is passed when passing an Option const utxos = await this.lucid.wallet.getUtxosCore(); + // We don't free `changeAddress` as it is passed as an Option to the txBuilder and the ownership is passed when passing an Option const changeAddress: C.Address = addressFromWithNetworkCheck( options?.change?.address || (await this.lucid.wallet.address()), this.lucid ); - bucket.push(utxos, changeAddress); if (options?.coinSelection || options?.coinSelection === undefined) { this.txBuilder.add_inputs_from( From d5508b7e75292d5cc7bddcad613032edd29ac17b Mon Sep 17 00:00:00 2001 From: yHSJ Date: Sat, 4 Nov 2023 14:25:51 -0400 Subject: [PATCH 16/54] feat: manage memory in utils.validatorToAddress --- src/utils/utils.ts | 302 +++++++++++++++++++++++++-------------------- 1 file changed, 165 insertions(+), 137 deletions(-) diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 414baee6..9266bc8c 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -40,6 +40,7 @@ import { unixTimeToEnclosingSlot, } from "../plutus/time.ts"; import { Data } from "../plutus/data.ts"; +import { FreeableBucket, Freeables } from "./freeable.ts"; export class Utils { private lucid: Lucid; @@ -49,54 +50,82 @@ export class Utils { validatorToAddress( validator: SpendingValidator, - stakeCredential?: Credential, + stakeCredential?: Credential ): Address { - const validatorHash = this.validatorToScriptHash(validator); - if (stakeCredential) { - return C.BaseAddress.new( - networkToId(this.lucid.network), - C.StakeCredential.from_scripthash(C.ScriptHash.from_hex(validatorHash)), - stakeCredential.type === "Key" - ? C.StakeCredential.from_keyhash( - C.Ed25519KeyHash.from_hex(stakeCredential.hash), - ) - : C.StakeCredential.from_scripthash( - C.ScriptHash.from_hex(stakeCredential.hash), - ), - ) - .to_address() - .to_bech32(undefined); - } else { - return C.EnterpriseAddress.new( - networkToId(this.lucid.network), - C.StakeCredential.from_scripthash(C.ScriptHash.from_hex(validatorHash)), - ) - .to_address() - .to_bech32(undefined); + const bucket: FreeableBucket = []; + const networkId = networkToId(this.lucid.network); + try { + const validatorHash = this.validatorToScriptHash(validator); + if (stakeCredential) { + const validatorScriptHash = C.ScriptHash.from_hex(validatorHash); + bucket.push(validatorScriptHash); + const paymentPart = + C.StakeCredential.from_scripthash(validatorScriptHash); + bucket.push(paymentPart); + let stakePart: C.StakeCredential; + if (stakeCredential.type === "Key") { + const keyHash = C.Ed25519KeyHash.from_hex(stakeCredential.hash); + stakePart = C.StakeCredential.from_keyhash(keyHash); + keyHash.free(); + } else { + const scriptHash = C.ScriptHash.from_hex(stakeCredential.hash); + stakePart = C.StakeCredential.from_scripthash(scriptHash); + scriptHash.free(); + } + bucket.push(stakePart); + const baseAddress = C.BaseAddress.new( + networkId, + paymentPart, + stakePart + ); + bucket.push(baseAddress); + const address = baseAddress.to_address(); + bucket.push(address); + + return address.to_bech32(undefined); + } else { + const validatorScriptHash = C.ScriptHash.from_hex(validatorHash); + bucket.push(validatorScriptHash); + + const paymentPart = + C.StakeCredential.from_scripthash(validatorScriptHash); + bucket.push(paymentPart); + const enterpriseAddress = C.EnterpriseAddress.new( + networkId, + paymentPart + ); + bucket.push(enterpriseAddress); + const address = enterpriseAddress.to_address(); + bucket.push(address); + + return address.to_bech32(undefined); + } + } finally { + Freeables.free(...bucket); } } credentialToAddress( paymentCredential: Credential, - stakeCredential?: Credential, + stakeCredential?: Credential ): Address { if (stakeCredential) { return C.BaseAddress.new( networkToId(this.lucid.network), paymentCredential.type === "Key" ? C.StakeCredential.from_keyhash( - C.Ed25519KeyHash.from_hex(paymentCredential.hash), - ) + C.Ed25519KeyHash.from_hex(paymentCredential.hash) + ) : C.StakeCredential.from_scripthash( - C.ScriptHash.from_hex(paymentCredential.hash), - ), + C.ScriptHash.from_hex(paymentCredential.hash) + ), stakeCredential.type === "Key" ? C.StakeCredential.from_keyhash( - C.Ed25519KeyHash.from_hex(stakeCredential.hash), - ) + C.Ed25519KeyHash.from_hex(stakeCredential.hash) + ) : C.StakeCredential.from_scripthash( - C.ScriptHash.from_hex(stakeCredential.hash), - ), + C.ScriptHash.from_hex(stakeCredential.hash) + ) ) .to_address() .to_bech32(undefined); @@ -105,11 +134,11 @@ export class Utils { networkToId(this.lucid.network), paymentCredential.type === "Key" ? C.StakeCredential.from_keyhash( - C.Ed25519KeyHash.from_hex(paymentCredential.hash), - ) + C.Ed25519KeyHash.from_hex(paymentCredential.hash) + ) : C.StakeCredential.from_scripthash( - C.ScriptHash.from_hex(paymentCredential.hash), - ), + C.ScriptHash.from_hex(paymentCredential.hash) + ) ) .to_address() .to_bech32(undefined); @@ -117,12 +146,12 @@ export class Utils { } validatorToRewardAddress( - validator: CertificateValidator | WithdrawalValidator, + validator: CertificateValidator | WithdrawalValidator ): RewardAddress { const validatorHash = this.validatorToScriptHash(validator); return C.RewardAddress.new( networkToId(this.lucid.network), - C.StakeCredential.from_scripthash(C.ScriptHash.from_hex(validatorHash)), + C.StakeCredential.from_scripthash(C.ScriptHash.from_hex(validatorHash)) ) .to_address() .to_bech32(undefined); @@ -133,11 +162,11 @@ export class Utils { networkToId(this.lucid.network), stakeCredential.type === "Key" ? C.StakeCredential.from_keyhash( - C.Ed25519KeyHash.from_hex(stakeCredential.hash), - ) + C.Ed25519KeyHash.from_hex(stakeCredential.hash) + ) : C.StakeCredential.from_scripthash( - C.ScriptHash.from_hex(stakeCredential.hash), - ), + C.ScriptHash.from_hex(stakeCredential.hash) + ) ) .to_address() .to_bech32(undefined); @@ -151,13 +180,13 @@ export class Utils { .to_hex(); case "PlutusV1": return C.PlutusScript.from_bytes( - fromHex(applyDoubleCborEncoding(validator.script)), + fromHex(applyDoubleCborEncoding(validator.script)) ) .hash(C.ScriptHashNamespace.PlutusV1) .to_hex(); case "PlutusV2": return C.PlutusScript.from_bytes( - fromHex(applyDoubleCborEncoding(validator.script)), + fromHex(applyDoubleCborEncoding(validator.script)) ) .hash(C.ScriptHashNamespace.PlutusV2) .to_hex(); @@ -199,7 +228,7 @@ export class Utils { unixTimeToSlot(unixTime: UnixTime): Slot { return unixTimeToEnclosingSlot( unixTime, - SLOT_CONFIG_NETWORK[this.lucid.network], + SLOT_CONFIG_NETWORK[this.lucid.network] ); } @@ -246,33 +275,30 @@ export function getAddressDetails(address: string): AddressDetails { // Base Address try { const parsedAddress = C.BaseAddress.from_address( - addressFromHexOrBech32(address), + addressFromHexOrBech32(address) )!; const paymentCredential: Credential = parsedAddress.payment_cred().kind() === 0 ? { - type: "Key", - hash: toHex( - parsedAddress.payment_cred().to_keyhash()!.to_bytes(), - ), - } + type: "Key", + hash: toHex(parsedAddress.payment_cred().to_keyhash()!.to_bytes()), + } : { - type: "Script", - hash: toHex( - parsedAddress.payment_cred().to_scripthash()!.to_bytes(), - ), - }; - const stakeCredential: Credential = parsedAddress.stake_cred().kind() === 0 - ? { - type: "Key", - hash: toHex(parsedAddress.stake_cred().to_keyhash()!.to_bytes()), - } - : { - type: "Script", - hash: toHex( - parsedAddress.stake_cred().to_scripthash()!.to_bytes(), - ), - }; + type: "Script", + hash: toHex( + parsedAddress.payment_cred().to_scripthash()!.to_bytes() + ), + }; + const stakeCredential: Credential = + parsedAddress.stake_cred().kind() === 0 + ? { + type: "Key", + hash: toHex(parsedAddress.stake_cred().to_keyhash()!.to_bytes()), + } + : { + type: "Script", + hash: toHex(parsedAddress.stake_cred().to_scripthash()!.to_bytes()), + }; return { type: "Base", networkId: parsedAddress.to_address().network_id(), @@ -283,27 +309,27 @@ export function getAddressDetails(address: string): AddressDetails { paymentCredential, stakeCredential, }; - } catch (_e) { /* pass */ } + } catch (_e) { + /* pass */ + } // Enterprise Address try { const parsedAddress = C.EnterpriseAddress.from_address( - addressFromHexOrBech32(address), + addressFromHexOrBech32(address) )!; const paymentCredential: Credential = parsedAddress.payment_cred().kind() === 0 ? { - type: "Key", - hash: toHex( - parsedAddress.payment_cred().to_keyhash()!.to_bytes(), - ), - } + type: "Key", + hash: toHex(parsedAddress.payment_cred().to_keyhash()!.to_bytes()), + } : { - type: "Script", - hash: toHex( - parsedAddress.payment_cred().to_scripthash()!.to_bytes(), - ), - }; + type: "Script", + hash: toHex( + parsedAddress.payment_cred().to_scripthash()!.to_bytes() + ), + }; return { type: "Enterprise", networkId: parsedAddress.to_address().network_id(), @@ -313,27 +339,27 @@ export function getAddressDetails(address: string): AddressDetails { }, paymentCredential, }; - } catch (_e) { /* pass */ } + } catch (_e) { + /* pass */ + } // Pointer Address try { const parsedAddress = C.PointerAddress.from_address( - addressFromHexOrBech32(address), + addressFromHexOrBech32(address) )!; const paymentCredential: Credential = parsedAddress.payment_cred().kind() === 0 ? { - type: "Key", - hash: toHex( - parsedAddress.payment_cred().to_keyhash()!.to_bytes(), - ), - } + type: "Key", + hash: toHex(parsedAddress.payment_cred().to_keyhash()!.to_bytes()), + } : { - type: "Script", - hash: toHex( - parsedAddress.payment_cred().to_scripthash()!.to_bytes(), - ), - }; + type: "Script", + hash: toHex( + parsedAddress.payment_cred().to_scripthash()!.to_bytes() + ), + }; return { type: "Pointer", networkId: parsedAddress.to_address().network_id(), @@ -343,27 +369,27 @@ export function getAddressDetails(address: string): AddressDetails { }, paymentCredential, }; - } catch (_e) { /* pass */ } + } catch (_e) { + /* pass */ + } // Reward Address try { const parsedAddress = C.RewardAddress.from_address( - addressFromHexOrBech32(address), + addressFromHexOrBech32(address) )!; const stakeCredential: Credential = parsedAddress.payment_cred().kind() === 0 ? { - type: "Key", - hash: toHex( - parsedAddress.payment_cred().to_keyhash()!.to_bytes(), - ), - } + type: "Key", + hash: toHex(parsedAddress.payment_cred().to_keyhash()!.to_bytes()), + } : { - type: "Script", - hash: toHex( - parsedAddress.payment_cred().to_scripthash()!.to_bytes(), - ), - }; + type: "Script", + hash: toHex( + parsedAddress.payment_cred().to_scripthash()!.to_bytes() + ), + }; return { type: "Reward", networkId: parsedAddress.to_address().network_id(), @@ -373,7 +399,9 @@ export function getAddressDetails(address: string): AddressDetails { }, stakeCredential, }; - } catch (_e) { /* pass */ } + } catch (_e) { + /* pass */ + } // Limited support for Byron addresses try { @@ -397,7 +425,9 @@ export function getAddressDetails(address: string): AddressDetails { hex: toHex(parsedAddress.to_address().to_bytes()), }, }; - } catch (_e) { /* pass */ } + } catch (_e) { + /* pass */ + } throw new Error("No address type matched for: " + address); } @@ -406,7 +436,7 @@ export function paymentCredentialOf(address: Address): Credential { const { paymentCredential } = getAddressDetails(address); if (!paymentCredential) { throw new Error( - "The specified address does not contain a payment credential.", + "The specified address does not contain a payment credential." ); } return paymentCredential; @@ -416,7 +446,7 @@ export function stakeCredentialOf(rewardAddress: RewardAddress): Credential { const { stakeCredential } = getAddressDetails(rewardAddress); if (!stakeCredential) { throw new Error( - "The specified address does not contain a stake credential.", + "The specified address does not contain a stake credential." ); } return stakeCredential; @@ -459,8 +489,8 @@ export function assetsToValue(assets: Assets): C.Value { new Set( units .filter((unit) => unit !== "lovelace") - .map((unit) => unit.slice(0, 56)), - ), + .map((unit) => unit.slice(0, 56)) + ) ); policies.forEach((policy) => { const policyUnits = units.filter((unit) => unit.slice(0, 56) === policy); @@ -468,13 +498,13 @@ export function assetsToValue(assets: Assets): C.Value { policyUnits.forEach((unit) => { assetsValue.insert( C.AssetName.new(fromHex(unit.slice(56))), - C.BigNum.from_str(assets[unit].toString()), + C.BigNum.from_str(assets[unit].toString()) ); }); multiAsset.insert(C.ScriptHash.from_bytes(fromHex(policy)), assetsValue); }); const value = C.Value.new( - C.BigNum.from_str(lovelace ? lovelace.toString() : "0"), + C.BigNum.from_str(lovelace ? lovelace.toString() : "0") ); if (units.length > 1 || !lovelace) value.set_multiasset(multiAsset); return value; @@ -507,23 +537,23 @@ export function toScriptRef(script: Script): C.ScriptRef { switch (script.type) { case "Native": return C.ScriptRef.new( - C.Script.new_native(C.NativeScript.from_bytes(fromHex(script.script))), + C.Script.new_native(C.NativeScript.from_bytes(fromHex(script.script))) ); case "PlutusV1": return C.ScriptRef.new( C.Script.new_plutus_v1( C.PlutusScript.from_bytes( - fromHex(applyDoubleCborEncoding(script.script)), - ), - ), + fromHex(applyDoubleCborEncoding(script.script)) + ) + ) ); case "PlutusV2": return C.ScriptRef.new( C.Script.new_plutus_v2( C.PlutusScript.from_bytes( - fromHex(applyDoubleCborEncoding(script.script)), - ), - ), + fromHex(applyDoubleCborEncoding(script.script)) + ) + ) ); default: throw new Error("No variant matched."); @@ -541,15 +571,13 @@ export function utxoToCore(utxo: UTxO): C.TransactionUnspentOutput { const output = C.TransactionOutput.new(address, assetsToValue(utxo.assets)); if (utxo.datumHash) { output.set_datum( - C.Datum.new_data_hash(C.DataHash.from_bytes(fromHex(utxo.datumHash))), + C.Datum.new_data_hash(C.DataHash.from_bytes(fromHex(utxo.datumHash))) ); } // inline datum if (!utxo.datumHash && utxo.datum) { output.set_datum( - C.Datum.new_data( - C.Data.new(C.PlutusData.from_bytes(fromHex(utxo.datum))), - ), + C.Datum.new_data(C.Data.new(C.PlutusData.from_bytes(fromHex(utxo.datum)))) ); } @@ -560,9 +588,9 @@ export function utxoToCore(utxo: UTxO): C.TransactionUnspentOutput { return C.TransactionUnspentOutput.new( C.TransactionInput.new( C.TransactionHash.from_bytes(fromHex(utxo.txHash)), - C.BigNum.from_str(utxo.outputIndex.toString()), + C.BigNum.from_str(utxo.outputIndex.toString()) ), - output, + output ); } @@ -575,9 +603,11 @@ export function coreToUtxo(coreUtxo: C.TransactionUnspentOutput): UTxO { ? coreUtxo.output().address().as_byron()?.to_base58()! : coreUtxo.output().address().to_bech32(undefined), datumHash: coreUtxo.output()?.datum()?.as_data_hash()?.to_hex(), - datum: coreUtxo.output()?.datum()?.as_data() && + datum: + coreUtxo.output()?.datum()?.as_data() && toHex(coreUtxo.output().datum()!.as_data()!.get().to_bytes()), - scriptRef: coreUtxo.output()?.script_ref() && + scriptRef: + coreUtxo.output()?.script_ref() && fromScriptRef(coreUtxo.output().script_ref()!), }; } @@ -627,7 +657,7 @@ function checksum(num: string): string { export function toLabel(num: number): string { if (num < 0 || num > 65535) { throw new Error( - `Label ${num} out of range: min label 1 - max label 65535.`, + `Label ${num} out of range: min label 1 - max label 65535.` ); } const numHex = num.toString(16).padStart(4, "0"); @@ -650,7 +680,7 @@ export function fromLabel(label: string): number | null { export function toUnit( policyId: PolicyId, name?: string | null, - label?: number | null, + label?: number | null ): Unit { const hexLabel = Number.isInteger(label) ? toLabel(label!) : ""; const n = name ? name : ""; @@ -667,9 +697,7 @@ export function toUnit( * Splits unit into policy id, asset name (entire asset name), name (asset name without label) and label if applicable. * name will be returned in Hex. */ -export function fromUnit( - unit: Unit, -): { +export function fromUnit(unit: Unit): { policyId: PolicyId; assetName: string | null; name: string | null; @@ -696,8 +724,8 @@ export function nativeScriptFromJson(nativeScript: NativeScript): Script { C.encode_json_str_to_native_script( JSON.stringify(nativeScript), "", - C.ScriptSchema.Node, - ).to_bytes(), + C.ScriptSchema.Node + ).to_bytes() ), }; } @@ -705,14 +733,14 @@ export function nativeScriptFromJson(nativeScript: NativeScript): Script { export function applyParamsToScript( plutusScript: string, params: Exact<[...T]>, - type?: T, + type?: T ): string { const p = (type ? Data.castTo(params, type) : params) as Data[]; return toHex( C.apply_params_to_plutus_script( C.PlutusList.from_bytes(fromHex(Data.to(p))), - C.PlutusScript.from_bytes(fromHex(applyDoubleCborEncoding(plutusScript))), - ).to_bytes(), + C.PlutusScript.from_bytes(fromHex(applyDoubleCborEncoding(plutusScript))) + ).to_bytes() ); } @@ -720,7 +748,7 @@ export function applyParamsToScript( export function applyDoubleCborEncoding(script: string): string { try { C.PlutusScript.from_bytes( - C.PlutusScript.from_bytes(fromHex(script)).bytes(), + C.PlutusScript.from_bytes(fromHex(script)).bytes() ); return script; } catch (_e) { From c2b4db90b3bb9ef91bb6d08f1c69ad06fe0eabbf Mon Sep 17 00:00:00 2001 From: yHSJ Date: Sat, 4 Nov 2023 14:30:54 -0400 Subject: [PATCH 17/54] feat: manage memory in utils.credentialToAddress --- src/utils/utils.ts | 95 ++++++++++++++++++++++++++++++---------------- 1 file changed, 62 insertions(+), 33 deletions(-) diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 9266bc8c..5c0e5394 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -109,39 +109,68 @@ export class Utils { paymentCredential: Credential, stakeCredential?: Credential ): Address { - if (stakeCredential) { - return C.BaseAddress.new( - networkToId(this.lucid.network), - paymentCredential.type === "Key" - ? C.StakeCredential.from_keyhash( - C.Ed25519KeyHash.from_hex(paymentCredential.hash) - ) - : C.StakeCredential.from_scripthash( - C.ScriptHash.from_hex(paymentCredential.hash) - ), - stakeCredential.type === "Key" - ? C.StakeCredential.from_keyhash( - C.Ed25519KeyHash.from_hex(stakeCredential.hash) - ) - : C.StakeCredential.from_scripthash( - C.ScriptHash.from_hex(stakeCredential.hash) - ) - ) - .to_address() - .to_bech32(undefined); - } else { - return C.EnterpriseAddress.new( - networkToId(this.lucid.network), - paymentCredential.type === "Key" - ? C.StakeCredential.from_keyhash( - C.Ed25519KeyHash.from_hex(paymentCredential.hash) - ) - : C.StakeCredential.from_scripthash( - C.ScriptHash.from_hex(paymentCredential.hash) - ) - ) - .to_address() - .to_bech32(undefined); + const networkId = networkToId(this.lucid.network); + const bucket: FreeableBucket = []; + try { + if (stakeCredential) { + let paymentPart: C.StakeCredential; + let stakePart: C.StakeCredential; + if (paymentCredential.type === "Key") { + const keyHash = C.Ed25519KeyHash.from_hex(paymentCredential.hash); + bucket.push(keyHash); + paymentPart = C.StakeCredential.from_keyhash(keyHash); + } else { + const scriptHash = C.ScriptHash.from_hex(paymentCredential.hash); + bucket.push(scriptHash); + paymentPart = C.StakeCredential.from_scripthash(scriptHash); + } + bucket.push(paymentPart); + if (stakeCredential.type === "Key") { + const keyHash = C.Ed25519KeyHash.from_hex(stakeCredential.hash); + bucket.push(keyHash); + stakePart = C.StakeCredential.from_keyhash(keyHash); + } else { + const scriptHash = C.ScriptHash.from_hex(stakeCredential.hash); + bucket.push(scriptHash); + stakePart = C.StakeCredential.from_scripthash(scriptHash); + } + bucket.push(stakePart); + + const baseAddress = C.BaseAddress.new( + networkId, + paymentPart, + stakePart + ); + bucket.push(baseAddress); + const address = baseAddress.to_address(); + bucket.push(address); + + return address.to_bech32(undefined); + } else { + let paymentPart: C.StakeCredential; + if (paymentCredential.type === "Key") { + const keyHash = C.Ed25519KeyHash.from_hex(paymentCredential.hash); + bucket.push(keyHash); + paymentPart = C.StakeCredential.from_keyhash(keyHash); + } else { + const scriptHash = C.ScriptHash.from_hex(paymentCredential.hash); + bucket.push(scriptHash); + paymentPart = C.StakeCredential.from_scripthash(scriptHash); + } + bucket.push(paymentPart); + + const enterpriseAddress = C.EnterpriseAddress.new( + networkId, + paymentPart + ); + bucket.push(enterpriseAddress); + const address = enterpriseAddress.to_address(); + bucket.push(address); + + return address.to_bech32(undefined); + } + } finally { + Freeables.free(...bucket); } } From c811418dab7d2e9b9eff2e7b1bb2ce56bea60d31 Mon Sep 17 00:00:00 2001 From: yHSJ Date: Sat, 4 Nov 2023 14:33:21 -0400 Subject: [PATCH 18/54] feat: manage memory in utils.validatorToRewardAddress --- src/utils/utils.ts | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 5c0e5394..17150cb3 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -177,13 +177,23 @@ export class Utils { validatorToRewardAddress( validator: CertificateValidator | WithdrawalValidator ): RewardAddress { + const bucket: FreeableBucket = []; const validatorHash = this.validatorToScriptHash(validator); - return C.RewardAddress.new( + const scriptHash = C.ScriptHash.from_hex(validatorHash); + bucket.push(scriptHash); + const stakePart = C.StakeCredential.from_scripthash(scriptHash); + bucket.push(stakePart); + const rewardAddress = C.RewardAddress.new( networkToId(this.lucid.network), - C.StakeCredential.from_scripthash(C.ScriptHash.from_hex(validatorHash)) - ) - .to_address() - .to_bech32(undefined); + stakePart + ); + bucket.push(rewardAddress); + const address = rewardAddress.to_address(); + bucket.push(address); + const bech32 = address.to_bech32(undefined); + + Freeables.free(...bucket); + return bech32; } credentialToRewardAddress(stakeCredential: Credential): RewardAddress { From 70e4f0ae94799fc9ba43dfc74cf6baeb7b655c43 Mon Sep 17 00:00:00 2001 From: yHSJ Date: Sat, 4 Nov 2023 14:37:14 -0400 Subject: [PATCH 19/54] feat: manage memory in utils.credentialToRewardAddress --- src/utils/utils.ts | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 17150cb3..d942553b 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -197,18 +197,30 @@ export class Utils { } credentialToRewardAddress(stakeCredential: Credential): RewardAddress { - return C.RewardAddress.new( + const bucket: FreeableBucket = []; + let stakePart: C.StakeCredential; + if (stakeCredential.type === "Key") { + const keyHash = C.Ed25519KeyHash.from_hex(stakeCredential.hash); + bucket.push(keyHash); + stakePart = C.StakeCredential.from_keyhash(keyHash); + } else { + const scriptHash = C.ScriptHash.from_hex(stakeCredential.hash); + bucket.push(scriptHash); + stakePart = C.StakeCredential.from_scripthash(scriptHash); + } + bucket.push(stakePart); + + const rewardAddress = C.RewardAddress.new( networkToId(this.lucid.network), - stakeCredential.type === "Key" - ? C.StakeCredential.from_keyhash( - C.Ed25519KeyHash.from_hex(stakeCredential.hash) - ) - : C.StakeCredential.from_scripthash( - C.ScriptHash.from_hex(stakeCredential.hash) - ) - ) - .to_address() - .to_bech32(undefined); + stakePart + ); + bucket.push(rewardAddress); + const address = rewardAddress.to_address(); + bucket.push(address); + const bech32 = address.to_bech32(undefined); + Freeables.free(...bucket); + + return bech32; } validatorToScriptHash(validator: Validator): ScriptHash { From 716aa076ef838bdef03825dbe342b7407052cc49 Mon Sep 17 00:00:00 2001 From: yHSJ Date: Sat, 4 Nov 2023 14:40:20 -0400 Subject: [PATCH 20/54] feat: manage memory in utils.validatorToScriptHash --- src/utils/utils.ts | 54 ++++++++++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/src/utils/utils.ts b/src/utils/utils.ts index d942553b..3599a3f4 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -224,25 +224,41 @@ export class Utils { } validatorToScriptHash(validator: Validator): ScriptHash { - switch (validator.type) { - case "Native": - return C.NativeScript.from_bytes(fromHex(validator.script)) - .hash(C.ScriptHashNamespace.NativeScript) - .to_hex(); - case "PlutusV1": - return C.PlutusScript.from_bytes( - fromHex(applyDoubleCborEncoding(validator.script)) - ) - .hash(C.ScriptHashNamespace.PlutusV1) - .to_hex(); - case "PlutusV2": - return C.PlutusScript.from_bytes( - fromHex(applyDoubleCborEncoding(validator.script)) - ) - .hash(C.ScriptHashNamespace.PlutusV2) - .to_hex(); - default: - throw new Error("No variant matched"); + const bucket: FreeableBucket = []; + try { + switch (validator.type) { + case "Native": { + const nativeScript = C.NativeScript.from_bytes( + fromHex(validator.script) + ); + bucket.push(nativeScript); + const hash = nativeScript.hash(C.ScriptHashNamespace.NativeScript); + bucket.push(hash); + return hash.to_hex(); + } + case "PlutusV1": { + const plutusScript = C.PlutusScript.from_bytes( + fromHex(applyDoubleCborEncoding(validator.script)) + ); + bucket.push(plutusScript); + const hash = plutusScript.hash(C.ScriptHashNamespace.PlutusV1); + bucket.push(hash); + return hash.to_hex(); + } + case "PlutusV2": { + const plutusScript = C.PlutusScript.from_bytes( + fromHex(applyDoubleCborEncoding(validator.script)) + ); + bucket.push(plutusScript); + const hash = plutusScript.hash(C.ScriptHashNamespace.PlutusV2); + bucket.push(hash); + return hash.to_hex(); + } + default: + throw new Error("No variant matched"); + } + } finally { + Freeables.free(...bucket); } } From 0134e1e7713249a3749f118360752e577840348a Mon Sep 17 00:00:00 2001 From: yHSJ Date: Sat, 4 Nov 2023 14:41:20 -0400 Subject: [PATCH 21/54] feat: manage memory in utils.datumToHash --- src/utils/utils.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 3599a3f4..f265ddfc 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -267,7 +267,12 @@ export class Utils { } datumToHash(datum: Datum): DatumHash { - return C.hash_plutus_data(C.PlutusData.from_bytes(fromHex(datum))).to_hex(); + const plutusData = C.PlutusData.from_bytes(fromHex(datum)); + const hash = C.hash_plutus_data(plutusData); + plutusData.free(); + const datumHash = hash.to_hex(); + hash.free(); + return datumHash; } scriptHashToCredential(scriptHash: ScriptHash): Credential { From dbd7cc72d4619aa3a4855ccd2681e5ceb89e9617 Mon Sep 17 00:00:00 2001 From: yHSJ Date: Sat, 4 Nov 2023 14:55:16 -0400 Subject: [PATCH 22/54] feat: manage memory in utils.getAddressDetails --- src/utils/utils.ts | 363 +++++++++++++++++++++++++++------------------ 1 file changed, 215 insertions(+), 148 deletions(-) diff --git a/src/utils/utils.ts b/src/utils/utils.ts index f265ddfc..0139e9a1 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -344,164 +344,231 @@ function addressFromHexOrBech32(address: string): C.Address { /** Address can be in Bech32 or Hex. */ export function getAddressDetails(address: string): AddressDetails { - // Base Address + const bucket: FreeableBucket = []; + // wrapped in an outer try to ensure that memory is freed try { - const parsedAddress = C.BaseAddress.from_address( - addressFromHexOrBech32(address) - )!; - const paymentCredential: Credential = - parsedAddress.payment_cred().kind() === 0 - ? { - type: "Key", - hash: toHex(parsedAddress.payment_cred().to_keyhash()!.to_bytes()), - } - : { - type: "Script", - hash: toHex( - parsedAddress.payment_cred().to_scripthash()!.to_bytes() - ), - }; - const stakeCredential: Credential = - parsedAddress.stake_cred().kind() === 0 - ? { - type: "Key", - hash: toHex(parsedAddress.stake_cred().to_keyhash()!.to_bytes()), - } - : { - type: "Script", - hash: toHex(parsedAddress.stake_cred().to_scripthash()!.to_bytes()), - }; - return { - type: "Base", - networkId: parsedAddress.to_address().network_id(), - address: { - bech32: parsedAddress.to_address().to_bech32(undefined), - hex: toHex(parsedAddress.to_address().to_bytes()), - }, - paymentCredential, - stakeCredential, - }; - } catch (_e) { - /* pass */ - } + // Base Address + try { + const parsedAddress = addressFromHexOrBech32(address); + bucket.push(parsedAddress); + const baseAddress = C.BaseAddress.from_address(parsedAddress)!; + bucket.push(baseAddress); + + let paymentCredential: Credential; + const paymentCred = baseAddress.payment_cred(); + bucket.push(paymentCred); + if (paymentCred.kind() === 0) { + const keyHash = paymentCred.to_keyhash()!; + bucket.push(keyHash); + paymentCredential = { + type: "Key", + hash: toHex(keyHash.to_bytes()), + }; + } else { + const scriptHash = paymentCred.to_scripthash()!; + bucket.push(scriptHash); + paymentCredential = { + type: "Script", + hash: toHex(scriptHash.to_bytes()), + }; + } - // Enterprise Address - try { - const parsedAddress = C.EnterpriseAddress.from_address( - addressFromHexOrBech32(address) - )!; - const paymentCredential: Credential = - parsedAddress.payment_cred().kind() === 0 - ? { - type: "Key", - hash: toHex(parsedAddress.payment_cred().to_keyhash()!.to_bytes()), - } - : { - type: "Script", - hash: toHex( - parsedAddress.payment_cred().to_scripthash()!.to_bytes() - ), - }; - return { - type: "Enterprise", - networkId: parsedAddress.to_address().network_id(), - address: { - bech32: parsedAddress.to_address().to_bech32(undefined), - hex: toHex(parsedAddress.to_address().to_bytes()), - }, - paymentCredential, - }; - } catch (_e) { - /* pass */ - } + let stakeCredential: Credential; + const stakeCred = baseAddress.stake_cred(); + bucket.push(stakeCred); + if (stakeCred.kind() === 0) { + const keyHash = stakeCred.to_keyhash()!; + bucket.push(keyHash); + stakeCredential = { + type: "Key", + hash: toHex(keyHash.to_bytes()), + }; + } else { + const scriptHash = stakeCred.to_scripthash()!; + bucket.push(scriptHash); + stakeCredential = { + type: "Script", + hash: toHex(scriptHash.to_bytes()), + }; + } - // Pointer Address - try { - const parsedAddress = C.PointerAddress.from_address( - addressFromHexOrBech32(address) - )!; - const paymentCredential: Credential = - parsedAddress.payment_cred().kind() === 0 - ? { - type: "Key", - hash: toHex(parsedAddress.payment_cred().to_keyhash()!.to_bytes()), - } - : { - type: "Script", - hash: toHex( - parsedAddress.payment_cred().to_scripthash()!.to_bytes() - ), - }; - return { - type: "Pointer", - networkId: parsedAddress.to_address().network_id(), - address: { - bech32: parsedAddress.to_address().to_bech32(undefined), - hex: toHex(parsedAddress.to_address().to_bytes()), - }, - paymentCredential, - }; - } catch (_e) { - /* pass */ - } + const cAddress = baseAddress.to_address(); + bucket.push(cAddress); - // Reward Address - try { - const parsedAddress = C.RewardAddress.from_address( - addressFromHexOrBech32(address) - )!; - const stakeCredential: Credential = - parsedAddress.payment_cred().kind() === 0 - ? { - type: "Key", - hash: toHex(parsedAddress.payment_cred().to_keyhash()!.to_bytes()), - } - : { - type: "Script", - hash: toHex( - parsedAddress.payment_cred().to_scripthash()!.to_bytes() - ), - }; - return { - type: "Reward", - networkId: parsedAddress.to_address().network_id(), - address: { - bech32: parsedAddress.to_address().to_bech32(undefined), - hex: toHex(parsedAddress.to_address().to_bytes()), - }, - stakeCredential, - }; - } catch (_e) { - /* pass */ - } + return { + type: "Base", + networkId: cAddress.network_id(), + address: { + bech32: cAddress.to_bech32(undefined), + hex: toHex(cAddress.to_bytes()), + }, + paymentCredential, + stakeCredential, + }; + } catch (_e) { + /* pass */ + } - // Limited support for Byron addresses - try { - const parsedAddress = ((address: string): C.ByronAddress => { - try { - return C.ByronAddress.from_bytes(fromHex(address)); - } catch (_e) { + // Enterprise Address + try { + const parsedAddress = addressFromHexOrBech32(address); + bucket.push(parsedAddress); + const enterpriseAddress = + C.EnterpriseAddress.from_address(parsedAddress)!; + bucket.push(enterpriseAddress); + + let paymentCredential: Credential; + const paymentCred = enterpriseAddress.payment_cred(); + bucket.push(paymentCred); + if (paymentCred.kind() === 0) { + const keyHash = paymentCred.to_keyhash()!; + bucket.push(keyHash); + paymentCredential = { + type: "Key", + hash: toHex(keyHash.to_bytes()), + }; + } else { + const scriptHash = paymentCred.to_scripthash()!; + bucket.push(scriptHash); + paymentCredential = { + type: "Script", + hash: toHex(scriptHash.to_bytes()), + }; + } + + const cAddress = enterpriseAddress.to_address(); + bucket.push(cAddress); + return { + type: "Enterprise", + networkId: cAddress.network_id(), + address: { + bech32: cAddress.to_bech32(undefined), + hex: toHex(cAddress.to_bytes()), + }, + paymentCredential, + }; + } catch (_e) { + /* pass */ + } + + // Pointer Address + try { + const parsedAddress = addressFromHexOrBech32(address); + bucket.push(parsedAddress); + const pointerAddress = C.PointerAddress.from_address(parsedAddress)!; + bucket.push(pointerAddress); + + let paymentCredential: Credential; + const paymentCred = pointerAddress.payment_cred(); + bucket.push(paymentCred); + if (paymentCred.kind() === 0) { + const keyHash = paymentCred.to_keyhash()!; + bucket.push(keyHash); + paymentCredential = { + type: "Key", + hash: toHex(keyHash.to_bytes()), + }; + } else { + const scriptHash = paymentCred.to_scripthash()!; + bucket.push(scriptHash); + paymentCredential = { + type: "Script", + hash: toHex(scriptHash.to_bytes()), + }; + } + + const cAddress = pointerAddress.to_address(); + bucket.push(cAddress); + + return { + type: "Pointer", + networkId: cAddress.network_id(), + address: { + bech32: cAddress.to_bech32(undefined), + hex: toHex(cAddress.to_bytes()), + }, + paymentCredential, + }; + } catch (_e) { + /* pass */ + } + + // Reward Address + try { + const parsedAddress = addressFromHexOrBech32(address); + bucket.push(parsedAddress); + const rewardAddress = C.RewardAddress.from_address(parsedAddress)!; + bucket.push(rewardAddress); + + let stakeCredential: Credential; + const paymentCred = rewardAddress.payment_cred(); + bucket.push(paymentCred); + if (paymentCred.kind() === 0) { + const keyHash = paymentCred.to_keyhash()!; + bucket.push(keyHash); + stakeCredential = { + type: "Key", + hash: toHex(keyHash.to_bytes()), + }; + } else { + const scriptHash = paymentCred.to_scripthash()!; + bucket.push(scriptHash); + stakeCredential = { + type: "Script", + hash: toHex(scriptHash.to_bytes()), + }; + } + + const cAddress = rewardAddress.to_address(); + bucket.push(cAddress); + + return { + type: "Reward", + networkId: cAddress.network_id(), + address: { + bech32: cAddress.to_bech32(undefined), + hex: toHex(cAddress.to_bytes()), + }, + stakeCredential, + }; + } catch (_e) { + /* pass */ + } + + // Limited support for Byron addresses + try { + const parsedAddress = ((address: string): C.ByronAddress => { try { - return C.ByronAddress.from_base58(address); + return C.ByronAddress.from_bytes(fromHex(address)); } catch (_e) { - throw new Error("Could not deserialize address."); + try { + return C.ByronAddress.from_base58(address); + } catch (_e) { + throw new Error("Could not deserialize address."); + } } - } - })(address); + })(address); + bucket.push(parsedAddress); - return { - type: "Byron", - networkId: parsedAddress.network_id(), - address: { - bech32: "", - hex: toHex(parsedAddress.to_address().to_bytes()), - }, - }; - } catch (_e) { - /* pass */ - } + const cAddress = parsedAddress.to_address(); + bucket.push(cAddress); - throw new Error("No address type matched for: " + address); + return { + type: "Byron", + networkId: parsedAddress.network_id(), + address: { + bech32: "", + hex: toHex(cAddress.to_bytes()), + }, + }; + } catch (_e) { + /* pass */ + } + + throw new Error("No address type matched for: " + address); + } finally { + Freeables.free(...bucket); + } } export function paymentCredentialOf(address: Address): Credential { From 9be2d6ac6bcc2f3bfeafd236bb6222d7378dcf28 Mon Sep 17 00:00:00 2001 From: yHSJ Date: Sat, 4 Nov 2023 14:57:08 -0400 Subject: [PATCH 23/54] feat manage memory in utils.generatePrivateKey --- src/utils/utils.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 0139e9a1..575a177d 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -592,7 +592,10 @@ export function stakeCredentialOf(rewardAddress: RewardAddress): Credential { } export function generatePrivateKey(): PrivateKey { - return C.PrivateKey.generate_ed25519().to_bech32(); + const ed25519 = C.PrivateKey.generate_ed25519(); + const bech32 = ed25519.to_bech32(); + ed25519.free(); + return bech32; } export function generateSeedPhrase(): string { From b66355657e455517f460f4c209bd55cf95ef2230 Mon Sep 17 00:00:00 2001 From: yHSJ Date: Sat, 4 Nov 2023 14:59:08 -0400 Subject: [PATCH 24/54] feat: manage memory in utils.valueToAssets --- src/utils/utils.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 575a177d..ac2ac30d 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -603,23 +603,34 @@ export function generateSeedPhrase(): string { } export function valueToAssets(value: C.Value): Assets { + const bucket: FreeableBucket = []; const assets: Assets = {}; - assets["lovelace"] = BigInt(value.coin().to_str()); + const lovelace = value.coin(); + bucket.push(lovelace); + assets["lovelace"] = BigInt(lovelace.to_str()); const ma = value.multiasset(); + bucket.push(ma); if (ma) { const multiAssets = ma.keys(); + bucket.push(multiAssets); for (let j = 0; j < multiAssets.len(); j++) { const policy = multiAssets.get(j); + bucket.push(policy); const policyAssets = ma.get(policy)!; + bucket.push(policyAssets); const assetNames = policyAssets.keys(); + bucket.push(assetNames); for (let k = 0; k < assetNames.len(); k++) { const policyAsset = assetNames.get(k); + bucket.push(policyAsset); const quantity = policyAssets.get(policyAsset)!; + bucket.push(quantity); const unit = toHex(policy.to_bytes()) + toHex(policyAsset.name()); assets[unit] = BigInt(quantity.to_str()); } } } + Freeables.free(...bucket); return assets; } From 17bfece43f6844cd060696297e9d8c5d2ed5902b Mon Sep 17 00:00:00 2001 From: yHSJ Date: Sat, 4 Nov 2023 15:02:35 -0400 Subject: [PATCH 25/54] feat: manage memory in utils.assetsToValue --- src/utils/utils.ts | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/utils/utils.ts b/src/utils/utils.ts index ac2ac30d..a4df5eef 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -635,7 +635,9 @@ export function valueToAssets(value: C.Value): Assets { } export function assetsToValue(assets: Assets): C.Value { + const bucket: FreeableBucket = []; const multiAsset = C.MultiAsset.new(); + bucket.push(multiAsset); const lovelace = assets["lovelace"]; const units = Object.keys(assets); const policies = Array.from( @@ -649,17 +651,24 @@ export function assetsToValue(assets: Assets): C.Value { const policyUnits = units.filter((unit) => unit.slice(0, 56) === policy); const assetsValue = C.Assets.new(); policyUnits.forEach((unit) => { - assetsValue.insert( - C.AssetName.new(fromHex(unit.slice(56))), - C.BigNum.from_str(assets[unit].toString()) - ); + const assetName = C.AssetName.new(fromHex(unit.slice(56))); + bucket.push(assetName); + const quantity = C.BigNum.from_str(assets[unit].toString()); + bucket.push(quantity); + + assetsValue.insert(assetName, quantity); }); - multiAsset.insert(C.ScriptHash.from_bytes(fromHex(policy)), assetsValue); + const policyId = C.ScriptHash.from_bytes(fromHex(policy)); + bucket.push(policyId); + multiAsset.insert(policyId, assetsValue); }); - const value = C.Value.new( - C.BigNum.from_str(lovelace ? lovelace.toString() : "0") - ); + const coin = C.BigNum.from_str(lovelace ? lovelace.toString() : "0"); + bucket.push(coin); + + const value = C.Value.new(coin); if (units.length > 1 || !lovelace) value.set_multiasset(multiAsset); + + Freeables.free(...bucket); return value; } From 3b9e59fe1c8c09c828e7ce53636089931e7d6402 Mon Sep 17 00:00:00 2001 From: yHSJ Date: Sat, 4 Nov 2023 15:04:39 -0400 Subject: [PATCH 26/54] feat: manage memory in utils.fromScriptRef --- src/utils/utils.ts | 56 +++++++++++++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/src/utils/utils.ts b/src/utils/utils.ts index a4df5eef..541b42b8 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -40,7 +40,7 @@ import { unixTimeToEnclosingSlot, } from "../plutus/time.ts"; import { Data } from "../plutus/data.ts"; -import { FreeableBucket, Freeables } from "./freeable.ts"; +import { Freeable, FreeableBucket, Freeables } from "./freeable.ts"; export class Utils { private lucid: Lucid; @@ -673,25 +673,41 @@ export function assetsToValue(assets: Assets): C.Value { } export function fromScriptRef(scriptRef: C.ScriptRef): Script { - const kind = scriptRef.get().kind(); - switch (kind) { - case 0: - return { - type: "Native", - script: toHex(scriptRef.get().as_native()!.to_bytes()), - }; - case 1: - return { - type: "PlutusV1", - script: toHex(scriptRef.get().as_plutus_v1()!.to_bytes()), - }; - case 2: - return { - type: "PlutusV2", - script: toHex(scriptRef.get().as_plutus_v2()!.to_bytes()), - }; - default: - throw new Error("No variant matched."); + const bucket: FreeableBucket = []; + try { + const script = scriptRef.get(); + bucket.push(script); + const kind = script.kind(); + switch (kind) { + case 0: { + const native = script.as_native()!; + bucket.push(native); + return { + type: "Native", + script: toHex(native.to_bytes()), + }; + } + case 1: { + const plutusV1 = script.as_plutus_v1()!; + bucket.push(plutusV1); + return { + type: "PlutusV1", + script: toHex(plutusV1.to_bytes()), + }; + } + case 2: { + const plutusV2 = script.as_plutus_v2()!; + bucket.push(plutusV2); + return { + type: "PlutusV2", + script: toHex(plutusV2.to_bytes()), + }; + } + default: + throw new Error("No variant matched."); + } + } finally { + Freeables.free(...bucket); } } From 109ba8b6f7708e1dc17d9fe82c30c1ef4b76c1fa Mon Sep 17 00:00:00 2001 From: yHSJ Date: Sat, 4 Nov 2023 15:07:14 -0400 Subject: [PATCH 27/54] feat: manage memory in utils.toScriptRef --- src/utils/utils.ts | 58 +++++++++++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 541b42b8..b0253a32 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -40,7 +40,7 @@ import { unixTimeToEnclosingSlot, } from "../plutus/time.ts"; import { Data } from "../plutus/data.ts"; -import { Freeable, FreeableBucket, Freeables } from "./freeable.ts"; +import { FreeableBucket, Freeables } from "./freeable.ts"; export class Utils { private lucid: Lucid; @@ -712,29 +712,39 @@ export function fromScriptRef(scriptRef: C.ScriptRef): Script { } export function toScriptRef(script: Script): C.ScriptRef { - switch (script.type) { - case "Native": - return C.ScriptRef.new( - C.Script.new_native(C.NativeScript.from_bytes(fromHex(script.script))) - ); - case "PlutusV1": - return C.ScriptRef.new( - C.Script.new_plutus_v1( - C.PlutusScript.from_bytes( - fromHex(applyDoubleCborEncoding(script.script)) - ) - ) - ); - case "PlutusV2": - return C.ScriptRef.new( - C.Script.new_plutus_v2( - C.PlutusScript.from_bytes( - fromHex(applyDoubleCborEncoding(script.script)) - ) - ) - ); - default: - throw new Error("No variant matched."); + const bucket: FreeableBucket = []; + try { + switch (script.type) { + case "Native": { + const nativeScript = C.NativeScript.from_bytes(fromHex(script.script)); + bucket.push(nativeScript); + const cScript = C.Script.new_native(nativeScript); + bucket.push(cScript); + return C.ScriptRef.new(cScript); + } + case "PlutusV1": { + const plutusScript = C.PlutusScript.from_bytes( + fromHex(applyDoubleCborEncoding(script.script)) + ); + bucket.push(plutusScript); + const cScript = C.Script.new_plutus_v1(plutusScript); + bucket.push(cScript); + return C.ScriptRef.new(cScript); + } + case "PlutusV2": { + const plutusScript = C.PlutusScript.from_bytes( + fromHex(applyDoubleCborEncoding(script.script)) + ); + bucket.push(plutusScript); + const cScript = C.Script.new_plutus_v2(plutusScript); + bucket.push(cScript); + return C.ScriptRef.new(cScript); + } + default: + throw new Error("No variant matched."); + } + } finally { + Freeables.free(...bucket); } } From e47205fb935fef29f299067c7d0da170147fc270 Mon Sep 17 00:00:00 2001 From: yHSJ Date: Sat, 4 Nov 2023 15:17:14 -0400 Subject: [PATCH 28/54] feat: manage memory in utils.coreToUtxo --- src/utils/utils.ts | 115 +++++++++++++++++++++++++++++++++------------ 1 file changed, 85 insertions(+), 30 deletions(-) diff --git a/src/utils/utils.ts b/src/utils/utils.ts index b0253a32..5b9cea1c 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -749,55 +749,110 @@ export function toScriptRef(script: Script): C.ScriptRef { } export function utxoToCore(utxo: UTxO): C.TransactionUnspentOutput { + const bucket: FreeableBucket = []; const address: C.Address = (() => { try { return C.Address.from_bech32(utxo.address); } catch (_e) { - return C.ByronAddress.from_base58(utxo.address).to_address(); + const byronAddress = C.ByronAddress.from_base58(utxo.address); + bucket.push(byronAddress); + return byronAddress.to_address(); } })(); - const output = C.TransactionOutput.new(address, assetsToValue(utxo.assets)); + bucket.push(address); + const value = assetsToValue(utxo.assets); + bucket.push(address); + const output = C.TransactionOutput.new(address, value); + bucket.push(output); if (utxo.datumHash) { - output.set_datum( - C.Datum.new_data_hash(C.DataHash.from_bytes(fromHex(utxo.datumHash))) - ); + const dataHash = C.DataHash.from_bytes(fromHex(utxo.datumHash)); + bucket.push(dataHash); + const datum = C.Datum.new_data_hash(dataHash); + bucket.push(datum); + output.set_datum(datum); } // inline datum if (!utxo.datumHash && utxo.datum) { - output.set_datum( - C.Datum.new_data(C.Data.new(C.PlutusData.from_bytes(fromHex(utxo.datum)))) - ); + const plutusData = C.PlutusData.from_bytes(fromHex(utxo.datum)); + bucket.push(plutusData); + const data = C.Data.new(plutusData); + bucket.push(data); + const datum = C.Datum.new_data(data); + bucket.push(datum); + output.set_datum(datum); } if (utxo.scriptRef) { - output.set_script_ref(toScriptRef(utxo.scriptRef)); + const scriptRef = toScriptRef(utxo.scriptRef); + bucket.push(scriptRef); + output.set_script_ref(scriptRef); } - return C.TransactionUnspentOutput.new( - C.TransactionInput.new( - C.TransactionHash.from_bytes(fromHex(utxo.txHash)), - C.BigNum.from_str(utxo.outputIndex.toString()) - ), - output - ); + const hash = C.TransactionHash.from_bytes(fromHex(utxo.txHash)); + bucket.push(hash); + const index = C.BigNum.from_str(utxo.outputIndex.toString()); + bucket.push(index); + const input = C.TransactionInput.new(hash, index); + bucket.push(input); + const coreUtxo = C.TransactionUnspentOutput.new(input, output); + + Freeables.free(...bucket); + return coreUtxo; } export function coreToUtxo(coreUtxo: C.TransactionUnspentOutput): UTxO { - return { - txHash: toHex(coreUtxo.input().transaction_id().to_bytes()), - outputIndex: parseInt(coreUtxo.input().index().to_str()), - assets: valueToAssets(coreUtxo.output().amount()), - address: coreUtxo.output().address().as_byron() - ? coreUtxo.output().address().as_byron()?.to_base58()! - : coreUtxo.output().address().to_bech32(undefined), - datumHash: coreUtxo.output()?.datum()?.as_data_hash()?.to_hex(), - datum: - coreUtxo.output()?.datum()?.as_data() && - toHex(coreUtxo.output().datum()!.as_data()!.get().to_bytes()), - scriptRef: - coreUtxo.output()?.script_ref() && - fromScriptRef(coreUtxo.output().script_ref()!), + const bucket: FreeableBucket = []; + const input = coreUtxo.input(); + bucket.push(input); + const output = coreUtxo.output(); + bucket.push(output); + + const txId = input.transaction_id(); + bucket.push(txId); + const txHash = toHex(txId.to_bytes()); + + const index = input.index(); + bucket.push(index); + const outputIndex = parseInt(index.to_str()); + + const amount = output.amount(); + bucket.push(amount); + const assets = valueToAssets(amount); + + const cAddress = output.address(); + bucket.push(cAddress); + const byronAddress = cAddress.as_byron(); + bucket.push(byronAddress); + const address = byronAddress + ? byronAddress.to_base58() + : cAddress.to_bech32(undefined); + + const cDatum = output.datum(); + bucket.push(cDatum); + const dataHash = cDatum?.as_data_hash(); + bucket.push(dataHash); + const datumHash = dataHash?.to_hex(); + + const cDatumData = cDatum?.as_data(); + bucket.push(cDatumData); + const plutusData = cDatumData?.get(); + bucket.push(plutusData); + const datum = plutusData && toHex(plutusData.to_bytes()); + const cScriptRef = output.script_ref(); + bucket.push(cScriptRef); + const scriptRef = cScriptRef && fromScriptRef(cScriptRef); + const utxo = { + txHash, + outputIndex, + assets, + address, + datumHash, + datum, + scriptRef, }; + + Freeables.free(...bucket); + return utxo; } export function networkToId(network: Network): number { From cf18ff194198608e05c894b03d4d34b9ddd7929b Mon Sep 17 00:00:00 2001 From: yHSJ Date: Sat, 4 Nov 2023 15:18:35 -0400 Subject: [PATCH 29/54] feat: manage memory in utils.toPublicKey --- src/utils/utils.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 5b9cea1c..2bca7733 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -889,7 +889,12 @@ export function fromText(text: string): string { } export function toPublicKey(privateKey: PrivateKey): PublicKey { - return C.PrivateKey.from_bech32(privateKey).to_public().to_bech32(); + const sKey = C.PrivateKey.from_bech32(privateKey); + const vKey = sKey.to_public(); + const bech32 = vKey.to_bech32(); + Freeables.free(sKey, vKey); + + return bech32; } /** Padded number in Hex. */ From 3a13927d8c7c05b10c90accf0974f04229505dc4 Mon Sep 17 00:00:00 2001 From: yHSJ Date: Sat, 4 Nov 2023 15:20:54 -0400 Subject: [PATCH 30/54] feat: manage memory in utils.nativeScriptFromJson --- src/utils/utils.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 2bca7733..6a0f66b4 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -966,15 +966,17 @@ export function fromUnit(unit: Unit): { * It follows this Json format: https://github.com/input-output-hk/cardano-node/blob/master/doc/reference/simple-scripts.md */ export function nativeScriptFromJson(nativeScript: NativeScript): Script { + const cNativeScript = C.encode_json_str_to_native_script( + JSON.stringify(nativeScript), + "", + C.ScriptSchema.Node + ); + const script = toHex(cNativeScript.to_bytes()); + cNativeScript.free(); + return { type: "Native", - script: toHex( - C.encode_json_str_to_native_script( - JSON.stringify(nativeScript), - "", - C.ScriptSchema.Node - ).to_bytes() - ), + script, }; } From c8e93d63fa5cca149ed5534f13af0abb36df11ea Mon Sep 17 00:00:00 2001 From: yHSJ Date: Sat, 4 Nov 2023 15:22:05 -0400 Subject: [PATCH 31/54] feat: manage memory in utils.applyParamsToScript --- src/utils/utils.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 6a0f66b4..79206647 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -986,12 +986,15 @@ export function applyParamsToScript( type?: T ): string { const p = (type ? Data.castTo(params, type) : params) as Data[]; - return toHex( - C.apply_params_to_plutus_script( - C.PlutusList.from_bytes(fromHex(Data.to(p))), - C.PlutusScript.from_bytes(fromHex(applyDoubleCborEncoding(plutusScript))) - ).to_bytes() + const cPlutusScript = C.PlutusScript.from_bytes( + fromHex(applyDoubleCborEncoding(plutusScript)) ); + const cParams = C.PlutusList.from_bytes(fromHex(Data.to(p))); + const cScript = C.apply_params_to_plutus_script(cParams, cPlutusScript); + const script = toHex(cScript.to_bytes()); + Freeables.free(cPlutusScript, cParams, cScript); + + return script; } /** Returns double cbor encoded script. If script is already double cbor encoded it's returned as it is. */ From 51b58ab763d54ec48d5225cfcf702ccd156c7fab Mon Sep 17 00:00:00 2001 From: yHSJ Date: Sat, 4 Nov 2023 15:24:49 -0400 Subject: [PATCH 32/54] feat: manage memory in utils.applyDoubleCborEncoding --- src/utils/utils.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 79206647..75ef9db5 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1000,12 +1000,15 @@ export function applyParamsToScript( /** Returns double cbor encoded script. If script is already double cbor encoded it's returned as it is. */ export function applyDoubleCborEncoding(script: string): string { try { - C.PlutusScript.from_bytes( - C.PlutusScript.from_bytes(fromHex(script)).bytes() - ); + const plutusScript = C.PlutusScript.from_bytes(fromHex(script)); + const doublePlutusScript = C.PlutusScript.new(plutusScript.to_bytes()); + Freeables.free(plutusScript, doublePlutusScript); return script; } catch (_e) { - return toHex(C.PlutusScript.new(fromHex(script)).to_bytes()); + const plutusScript = C.PlutusScript.new(fromHex(script)); + const bytes = plutusScript.to_bytes(); + plutusScript.free(); + return toHex(bytes); } } From fe30d936d791badf197d3a8c75d8bbe986b0a275 Mon Sep 17 00:00:00 2001 From: yHSJ Date: Sat, 4 Nov 2023 15:46:14 -0400 Subject: [PATCH 33/54] feat: fix failing tests --- src/utils/utils.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 75ef9db5..a19e89a2 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -752,6 +752,7 @@ export function utxoToCore(utxo: UTxO): C.TransactionUnspentOutput { const bucket: FreeableBucket = []; const address: C.Address = (() => { try { + console.log("success"); return C.Address.from_bech32(utxo.address); } catch (_e) { const byronAddress = C.ByronAddress.from_base58(utxo.address); @@ -761,7 +762,7 @@ export function utxoToCore(utxo: UTxO): C.TransactionUnspentOutput { })(); bucket.push(address); const value = assetsToValue(utxo.assets); - bucket.push(address); + bucket.push(value); const output = C.TransactionOutput.new(address, value); bucket.push(output); if (utxo.datumHash) { @@ -986,17 +987,23 @@ export function applyParamsToScript( type?: T ): string { const p = (type ? Data.castTo(params, type) : params) as Data[]; + // cPlutusScript ownership is passed to rust, so don't free const cPlutusScript = C.PlutusScript.from_bytes( fromHex(applyDoubleCborEncoding(plutusScript)) ); const cParams = C.PlutusList.from_bytes(fromHex(Data.to(p))); const cScript = C.apply_params_to_plutus_script(cParams, cPlutusScript); const script = toHex(cScript.to_bytes()); - Freeables.free(cPlutusScript, cParams, cScript); + Freeables.free(cParams, cScript); return script; } +// return toHex( +// C.apply_params_to_plutus_script( +// C.PlutusList.from_bytes(fromHex(Data.to(p))), +// C.PlutusScript.from_bytes(fromHex(applyDoubleCborEncoding(plutusScript))) +// ).to_bytes() /** Returns double cbor encoded script. If script is already double cbor encoded it's returned as it is. */ export function applyDoubleCborEncoding(script: string): string { try { From 7aef11bd2d99d93bbeaa47c5bfc7d69c2d75bef7 Mon Sep 17 00:00:00 2001 From: yHSJ Date: Sat, 4 Nov 2023 15:57:44 -0400 Subject: [PATCH 34/54] feat: manage memory in data.to --- src/plutus/data.ts | 173 +++++++++++++++++++++++++-------------------- 1 file changed, 97 insertions(+), 76 deletions(-) diff --git a/src/plutus/data.ts b/src/plutus/data.ts index 92225b72..5e54f3a9 100644 --- a/src/plutus/data.ts +++ b/src/plutus/data.ts @@ -89,7 +89,7 @@ export const Data = { }, Array: function ( items: T, - options?: { minItems?: number; maxItems?: number; uniqueItems?: boolean }, + options?: { minItems?: number; maxItems?: number; uniqueItems?: boolean } ) { const array = Type.Array(items); replaceProperties(array, { dataType: "list", items }); @@ -103,7 +103,7 @@ export const Data = { Map: function ( keys: T, values: U, - options?: { minItems?: number; maxItems?: number }, + options?: { minItems?: number; maxItems?: number } ) { const map = Type.Unsafe, Data.Static>>({ dataType: "map", @@ -123,7 +123,7 @@ export const Data = { */ Object: function ( properties: T, - options?: { hasConstr?: boolean }, + options?: { hasConstr?: boolean } ) { const object = Type.Object(properties); replaceProperties(object, { @@ -138,8 +138,8 @@ export const Data = { }, ], }); - object.anyOf[0].hasConstr = typeof options?.hasConstr === "undefined" || - options.hasConstr; + object.anyOf[0].hasConstr = + typeof options?.hasConstr === "undefined" || options.hasConstr; return object; }, Enum: function (items: T[]) { @@ -148,27 +148,28 @@ export const Data = { anyOf: items.map((item, index) => item.anyOf[0].fields.length === 0 ? { - ...item.anyOf[0], - index, - } + ...item.anyOf[0], + index, + } : { - dataType: "constructor", - title: (() => { - const title = item.anyOf[0].fields[0].title; - if ( - (title as string).charAt(0) !== + dataType: "constructor", + title: (() => { + const title = item.anyOf[0].fields[0].title; + if ( + (title as string).charAt(0) !== (title as string).charAt(0).toUpperCase() - ) { - throw new Error( - `Enum '${title}' needs to start with an uppercase letter.`, - ); - } - return item.anyOf[0].fields[0].title; - })(), - index, - fields: item.anyOf[0].fields[0].items || - item.anyOf[0].fields[0].anyOf[0].fields, - } + ) { + throw new Error( + `Enum '${title}' needs to start with an uppercase letter.` + ); + } + return item.anyOf[0].fields[0].title; + })(), + index, + fields: + item.anyOf[0].fields[0].items || + item.anyOf[0].fields[0].anyOf[0].fields, + } ), }); return union; @@ -179,7 +180,7 @@ export const Data = { */ Tuple: function ( items: [...T], - options?: { hasConstr?: boolean }, + options?: { hasConstr?: boolean } ) { const tuple = Type.Tuple(items); replaceProperties(tuple, { @@ -198,7 +199,7 @@ export const Data = { (title as string).charAt(0) !== (title as string).charAt(0).toUpperCase() ) { throw new Error( - `Enum '${title}' needs to start with an uppercase letter.`, + `Enum '${title}' needs to start with an uppercase letter.` ); } const literal = Type.Literal(title); @@ -264,34 +265,51 @@ export const Data = { */ function to(data: Exact, type?: T): Datum | Redeemer { function serialize(data: Data): C.PlutusData { + const bucket: FreeableBucket = []; try { if (typeof data === "bigint") { - return C.PlutusData.new_integer(C.BigInt.from_str(data.toString())); + const integer = C.BigInt.from_str(data.toString()); + bucket.push(integer); + return C.PlutusData.new_integer(integer); } else if (typeof data === "string") { return C.PlutusData.new_bytes(fromHex(data)); } else if (data instanceof Constr) { const { index, fields } = data; const plutusList = C.PlutusList.new(); + bucket.push(plutusList); + fields.forEach((field) => { + const serializedField = serialize(field); + plutusList.add(serializedField); + bucket.push(serializedField); + }); - fields.forEach((field) => plutusList.add(serialize(field))); - - return C.PlutusData.new_constr_plutus_data( - C.ConstrPlutusData.new( - C.BigNum.from_str(index.toString()), - plutusList, - ), + const constrIndex = C.BigNum.from_str(index.toString()); + bucket.push(constrIndex); + const cosntrPlutusData = C.ConstrPlutusData.new( + constrIndex, + plutusList ); + bucket.push(cosntrPlutusData); + return C.PlutusData.new_constr_plutus_data(cosntrPlutusData); } else if (data instanceof Array) { const plutusList = C.PlutusList.new(); - - data.forEach((arg) => plutusList.add(serialize(arg))); + bucket.push(plutusList); + data.forEach((arg) => { + const serializedArg = serialize(arg); + plutusList.add(serializedArg); + bucket.push(serializedArg); + }); return C.PlutusData.new_list(plutusList); } else if (data instanceof Map) { const plutusMap = C.PlutusMap.new(); - + bucket.push(plutusMap); for (const [key, value] of data.entries()) { - plutusMap.insert(serialize(key), serialize(value)); + const serializedKey = serialize(key); + bucket.push(serializedKey); + const serializedValue = serialize(value); + bucket.push(serializedValue); + plutusMap.insert(serializedKey, serializedValue); } return C.PlutusData.new_map(plutusMap); @@ -299,10 +317,15 @@ function to(data: Exact, type?: T): Datum | Redeemer { throw new Error("Unsupported type"); } catch (error) { throw new Error("Could not serialize the data: " + error); + } finally { + Freeables.free(...bucket); } } const d = type ? castTo(data, type) : (data as Data); - return toHex(serialize(d).to_bytes()) as Datum | Redeemer; + const serializedD = serialize(d); + const result = toHex(serializedD.to_bytes()) as Datum | Redeemer; + serializedD.free(); + return result; } /** @@ -408,15 +431,14 @@ function toJson(plutusData: Data): Json { !isNaN(parseInt(data)) && data.slice(-1) === "n") ) { - const bigint = typeof data === "string" - ? BigInt(data.slice(0, -1)) - : data; + const bigint = + typeof data === "string" ? BigInt(data.slice(0, -1)) : data; return parseInt(bigint.toString()); } if (typeof data === "string") { try { return new TextDecoder(undefined, { fatal: true }).decode( - fromHex(data), + fromHex(data) ); } catch (_) { return "0x" + toHex(fromHex(data)); @@ -432,7 +454,7 @@ function toJson(plutusData: Data): Json { typeof convertedKey !== "number" ) { throw new Error( - "Unsupported type (Note: Only bytes or integers can be keys of a JSON object)", + "Unsupported type (Note: Only bytes or integers can be keys of a JSON object)" ); } tempJson[convertedKey] = fromData(value); @@ -440,7 +462,7 @@ function toJson(plutusData: Data): Json { return tempJson; } throw new Error( - "Unsupported type (Note: Constructor cannot be converted to JSON)", + "Unsupported type (Note: Constructor cannot be converted to JSON)" ); } return fromData(plutusData); @@ -484,14 +506,14 @@ function castFrom(data: Data, type: T): T { const fields: Record = {}; if (shape.fields.length !== data.fields.length) { throw new Error( - "Could not type cast to object. Fields do not match.", + "Could not type cast to object. Fields do not match." ); } shape.fields.forEach((field: Json, fieldIndex: number) => { const title = field.title || "wrapper"; if (/[A-Z]/.test(title[0])) { throw new Error( - "Could not type cast to object. Object properties need to start with a lowercase letter.", + "Could not type cast to object. Object properties need to start with a lowercase letter." ); } fields[title] = castFrom(data.fields[fieldIndex], field); @@ -510,7 +532,7 @@ function castFrom(data: Data, type: T): T { const title = field.title || "wrapper"; if (/[A-Z]/.test(title[0])) { throw new Error( - "Could not type cast to object. Object properties need to start with a lowercase letter.", + "Could not type cast to object. Object properties need to start with a lowercase letter." ); } fields[title] = castFrom(data[fieldIndex], field); @@ -530,7 +552,7 @@ function castFrom(data: Data, type: T): T { } const enumShape = shape.anyOf.find( - (entry: Json) => entry.index === data.index, + (entry: Json) => entry.index === data.index ); if (!enumShape || enumShape.fields.length !== data.fields.length) { throw new Error("Could not type cast to enum."); @@ -573,7 +595,7 @@ function castFrom(data: Data, type: T): T { } else { if (!/[A-Z]/.test(enumShape.title)) { throw new Error( - "Could not type cast to enum. Enums need to start with an uppercase letter.", + "Could not type cast to enum. Enums need to start with an uppercase letter." ); } @@ -584,14 +606,14 @@ function castFrom(data: Data, type: T): T { // check if named args const args = enumShape.fields[0].title ? Object.fromEntries( - enumShape.fields.map((field: Json, index: number) => [ - field.title, - castFrom(data.fields[index], field), - ]), - ) + enumShape.fields.map((field: Json, index: number) => [ + field.title, + castFrom(data.fields[index], field), + ]) + ) : enumShape.fields.map((field: Json, index: number) => - castFrom(data.fields[index], field) - ); + castFrom(data.fields[index], field) + ); return { [enumShape.title]: args, @@ -679,7 +701,7 @@ function castTo(struct: Exact, type: T): Data { const fields = shape.fields.map((field: Json) => castTo( (struct as Record)[field.title || "wrapper"], - field, + field ) ); return shape.hasConstr || shape.hasConstr === undefined @@ -711,14 +733,14 @@ function castTo(struct: Exact, type: T): Data { case "string": { if (!/[A-Z]/.test(struct[0])) { throw new Error( - "Could not type cast to enum. Enum needs to start with an uppercase letter.", + "Could not type cast to enum. Enum needs to start with an uppercase letter." ); } const enumIndex = (shape as TEnum).anyOf.findIndex( (s: TLiteral) => s.dataType === "constructor" && s.fields.length === 0 && - s.title === struct, + s.title === struct ); if (enumIndex === -1) throw new Error("Could not type cast to enum."); return new Constr(enumIndex, []); @@ -729,12 +751,11 @@ function castTo(struct: Exact, type: T): Data { if (!/[A-Z]/.test(structTitle)) { throw new Error( - "Could not type cast to enum. Enum needs to start with an uppercase letter.", + "Could not type cast to enum. Enum needs to start with an uppercase letter." ); } const enumEntry = shape.anyOf.find( - (s: Json) => - s.dataType === "constructor" && s.title === structTitle, + (s: Json) => s.dataType === "constructor" && s.title === structTitle ); if (!enumEntry) throw new Error("Could not type cast to enum."); @@ -746,14 +767,14 @@ function castTo(struct: Exact, type: T): Data { // check if named args args instanceof Array ? args.map((item, index) => - castTo(item, enumEntry.fields[index]) - ) + castTo(item, enumEntry.fields[index]) + ) : enumEntry.fields.map((entry: Json) => { - const [_, item]: [string, Json] = Object.entries(args).find( - ([title]) => title === entry.title, - )!; - return castTo(item, entry); - }), + const [_, item]: [string, Json] = Object.entries(args).find( + ([title]) => title === entry.title + )!; + return castTo(item, entry); + }) ); } } @@ -798,22 +819,22 @@ function castTo(struct: Exact, type: T): Data { function integerConstraints(integer: bigint, shape: TSchema) { if (shape.minimum && integer < BigInt(shape.minimum)) { throw new Error( - `Integer ${integer} is below the minimum ${shape.minimum}.`, + `Integer ${integer} is below the minimum ${shape.minimum}.` ); } if (shape.maximum && integer > BigInt(shape.maximum)) { throw new Error( - `Integer ${integer} is above the maxiumum ${shape.maximum}.`, + `Integer ${integer} is above the maxiumum ${shape.maximum}.` ); } if (shape.exclusiveMinimum && integer <= BigInt(shape.exclusiveMinimum)) { throw new Error( - `Integer ${integer} is below the exclusive minimum ${shape.exclusiveMinimum}.`, + `Integer ${integer} is below the exclusive minimum ${shape.exclusiveMinimum}.` ); } if (shape.exclusiveMaximum && integer >= BigInt(shape.exclusiveMaximum)) { throw new Error( - `Integer ${integer} is above the exclusive maximum ${shape.exclusiveMaximum}.`, + `Integer ${integer} is above the exclusive maximum ${shape.exclusiveMaximum}.` ); } } @@ -824,13 +845,13 @@ function bytesConstraints(bytes: string, shape: TSchema) { } if (shape.minLength && bytes.length / 2 < shape.minLength) { throw new Error( - `Bytes need to have a length of at least ${shape.minLength} bytes.`, + `Bytes need to have a length of at least ${shape.minLength} bytes.` ); } if (shape.maxLength && bytes.length / 2 > shape.maxLength) { throw new Error( - `Bytes can have a length of at most ${shape.minLength} bytes.`, + `Bytes can have a length of at most ${shape.minLength} bytes.` ); } } From c412b82bd9dc66702f7e176b92c258f024fb5112 Mon Sep 17 00:00:00 2001 From: yHSJ Date: Sat, 4 Nov 2023 16:29:46 -0400 Subject: [PATCH 35/54] feat: manage memory in emulator.submitTx --- src/provider/emulator.ts | 532 +++++++++++++++++++++++---------------- 1 file changed, 315 insertions(+), 217 deletions(-) diff --git a/src/provider/emulator.ts b/src/provider/emulator.ts index a6b5464b..efd311b1 100644 --- a/src/provider/emulator.ts +++ b/src/provider/emulator.ts @@ -20,6 +20,7 @@ import { UnixTime, UTxO, } from "../types/types.ts"; +import { FreeableBucket } from "../utils/freeable.ts"; import { PROTOCOL_PARAMETERS_DEFAULT } from "../utils/mod.ts"; import { coreToUtxo, @@ -54,7 +55,7 @@ export class Emulator implements Provider { assets: Assets; outputData?: OutputData; }[], - protocolParameters: ProtocolParameters = PROTOCOL_PARAMETERS_DEFAULT, + protocolParameters: ProtocolParameters = PROTOCOL_PARAMETERS_DEFAULT ) { const GENESIS_HASH = "00".repeat(32); this.blockHeight = 0; @@ -63,15 +64,12 @@ export class Emulator implements Provider { this.ledger = {}; accounts.forEach(({ address, assets, outputData }, index) => { if ( - [ - outputData?.hash, - outputData?.asHash, - outputData?.inline, - ].filter((b) => b) - .length > 1 + [outputData?.hash, outputData?.asHash, outputData?.inline].filter( + (b) => b + ).length > 1 ) { throw new Error( - "Not allowed to set hash, asHash and inline at the same time.", + "Not allowed to set hash, asHash and inline at the same time." ); } @@ -83,8 +81,8 @@ export class Emulator implements Provider { assets, datumHash: outputData?.asHash ? C.hash_plutus_data( - C.PlutusData.from_bytes(fromHex(outputData.asHash)), - ).to_hex() + C.PlutusData.from_bytes(fromHex(outputData.asHash)) + ).to_hex() : outputData?.hash, datum: outputData?.inline, scriptRef: outputData?.scriptRef, @@ -139,16 +137,12 @@ export class Emulator implements Provider { if (typeof addressOrCredential === "string") { return addressOrCredential === utxo.address ? utxo : []; } else { - const { paymentCredential } = getAddressDetails( - utxo.address, - ); + const { paymentCredential } = getAddressDetails(utxo.address); return paymentCredential?.hash === addressOrCredential.hash ? utxo : []; } }); - return Promise.resolve( - utxos, - ); + return Promise.resolve(utxos); } getProtocolParameters(): Promise { @@ -161,7 +155,7 @@ export class Emulator implements Provider { getUtxosWithUnit( addressOrCredential: Address | Credential, - unit: Unit, + unit: Unit ): Promise { const utxos: UTxO[] = Object.values(this.ledger).flatMap(({ utxo }) => { if (typeof addressOrCredential === "string") { @@ -169,26 +163,22 @@ export class Emulator implements Provider { ? utxo : []; } else { - const { paymentCredential } = getAddressDetails( - utxo.address, - ); + const { paymentCredential } = getAddressDetails(utxo.address); return paymentCredential?.hash === addressOrCredential.hash && - utxo.assets[unit] > 0n + utxo.assets[unit] > 0n ? utxo : []; } }); - return Promise.resolve( - utxos, - ); + return Promise.resolve(utxos); } getUtxosByOutRef(outRefs: OutRef[]): Promise { return Promise.resolve( - outRefs.flatMap((outRef) => - this.ledger[outRef.txHash + outRef.outputIndex]?.utxo || [] - ), + outRefs.flatMap( + (outRef) => this.ledger[outRef.txHash + outRef.outputIndex]?.utxo || [] + ) ); } @@ -224,17 +214,16 @@ export class Emulator implements Provider { * Stake keys need to be registered and delegated like on a real chain in order to receive rewards. */ distributeRewards(rewards: Lovelace) { - for ( - const [rewardAddress, { registeredStake, delegation }] of Object.entries( - this.chain, - ) - ) { + for (const [ + rewardAddress, + { registeredStake, delegation }, + ] of Object.entries(this.chain)) { if (registeredStake && delegation.poolId) { this.chain[rewardAddress] = { registeredStake, delegation: { poolId: delegation.poolId, - rewards: delegation.rewards += rewards, + rewards: (delegation.rewards += rewards), }, }; } @@ -260,31 +249,42 @@ export class Emulator implements Provider { - Validity interval */ + const bucket: FreeableBucket = []; const desTx = C.Transaction.from_bytes(fromHex(tx)); + bucket.push(desTx); const body = desTx.body(); + bucket.push(body); const witnesses = desTx.witness_set(); + bucket.push(witnesses); const datums = witnesses.plutus_data(); + bucket.push(datums); - const txHash = C.hash_transaction(body).to_hex(); + const transactionHash = C.hash_transaction(body); + bucket.push(transactionHash); + const txHash = transactionHash.to_hex(); // Validity interval // Lower bound is inclusive? // Upper bound is inclusive? - const lowerBound = body.validity_start_interval() - ? parseInt(body.validity_start_interval()!.to_str()) + const validityStartInterval = body.validity_start_interval(); + bucket.push(validityStartInterval); + const lowerBound = validityStartInterval + ? parseInt(validityStartInterval.to_str()) : null; - const upperBound = body.ttl() ? parseInt(body.ttl()!.to_str()) : null; + const ttl = body.ttl(); + bucket.push(ttl); + const upperBound = ttl ? parseInt(ttl.to_str()) : null; if (Number.isInteger(lowerBound) && this.slot < lowerBound!) { throw new Error( - `Lower bound (${lowerBound}) not in slot range (${this.slot}).`, + `Lower bound (${lowerBound}) not in slot range (${this.slot}).` ); } if (Number.isInteger(upperBound) && this.slot > upperBound!) { throw new Error( - `Upper bound (${upperBound}) not in slot range (${this.slot}).`, + `Upper bound (${upperBound}) not in slot range (${this.slot}).` ); } @@ -293,7 +293,10 @@ export class Emulator implements Provider { const table: Record = {}; for (let i = 0; i < (datums?.len() || 0); i++) { const datum = datums!.get(i); - const datumHash = C.hash_plutus_data(datum).to_hex(); + bucket.push(datum); + const plutusDataHash = C.hash_plutus_data(datum); + bucket.push(plutusDataHash); + const datumHash = plutusDataHash.to_hex(); table[datumHash] = toHex(datum.to_bytes()); } return table; @@ -304,15 +307,21 @@ export class Emulator implements Provider { // Witness keys const keyHashes = (() => { const keyHashes = []; - for (let i = 0; i < (witnesses.vkeys()?.len() || 0); i++) { - const witness = witnesses.vkeys()!.get(i); - const publicKey = witness.vkey().public_key(); - const keyHash = publicKey.hash().to_hex(); + const vkeys = witnesses.vkeys(); + bucket.push(vkeys); + for (let i = 0; i < (vkeys?.len() || 0); i++) { + const witness = vkeys!.get(i); + bucket.push(witness); + const vkey = witness.vkey(); + bucket.push(vkey); + const publicKey = vkey.public_key(); + bucket.push(publicKey); + const hash = publicKey.hash(); + bucket.push(hash); + const keyHash = hash.to_hex(); if (!publicKey.verify(fromHex(txHash), witness.signature())) { - throw new Error( - `Invalid vkey witness. Key hash: ${keyHash}`, - ); + throw new Error(`Invalid vkey witness. Key hash: ${keyHash}`); } keyHashes.push(keyHash); } @@ -321,35 +330,43 @@ export class Emulator implements Provider { // We only need this to verify native scripts. The check happens in the CML. const edKeyHashes = C.Ed25519KeyHashes.new(); - keyHashes.forEach((keyHash) => - edKeyHashes.add(C.Ed25519KeyHash.from_hex(keyHash)) - ); + bucket.push(edKeyHashes); + keyHashes.forEach((keyHash) => { + const ed25519KeyHash = C.Ed25519KeyHash.from_hex(keyHash); + bucket.push(ed25519KeyHash); + edKeyHashes.add(ed25519KeyHash); + }); const nativeHashes = (() => { const scriptHashes = []; - - for (let i = 0; i < (witnesses.native_scripts()?.len() || 0); i++) { - const witness = witnesses.native_scripts()!.get(i); - const scriptHash = witness.hash(C.ScriptHashNamespace.NativeScript) - .to_hex(); - - if ( - !witness.verify( - Number.isInteger(lowerBound) - ? C.BigNum.from_str(lowerBound!.toString()) - : undefined, - Number.isInteger(upperBound) - ? C.BigNum.from_str(upperBound!.toString()) - : undefined, - edKeyHashes, - ) - ) { + const nativeScripts = witnesses.native_scripts(); + bucket.push(nativeScripts); + for (let i = 0; i < (nativeScripts?.len() || 0); i++) { + const witness = nativeScripts!.get(i); + bucket.push(witness); + const hash = witness.hash(C.ScriptHashNamespace.NativeScript); + bucket.push(hash); + const scriptHash = hash.to_hex(); + + const lBound = Number.isInteger(lowerBound) + ? C.BigNum.from_str(lowerBound!.toString()) + : undefined; + bucket.push(lBound); + const uBound = Number.isInteger(upperBound) + ? C.BigNum.from_str(upperBound!.toString()) + : undefined; + bucket.push(uBound); + if (!witness.verify(lBound, uBound, edKeyHashes)) { throw new Error( - `Invalid native script witness. Script hash: ${scriptHash}`, + `Invalid native script witness. Script hash: ${scriptHash}` ); } - for (let i = 0; i < witness.get_required_signers().len(); i++) { - const keyHash = witness.get_required_signers().get(i).to_hex(); + const requiredSigners = witness.get_required_signers(); + bucket.push(requiredSigners); + for (let i = 0; i < requiredSigners.len(); i++) { + const hash = requiredSigners.get(i); + bucket.push(hash); + const keyHash = hash.to_hex(); consumedHashes.add(keyHash); } scriptHashes.push(scriptHash); @@ -362,17 +379,26 @@ export class Emulator implements Provider { const plutusHashes = (() => { const scriptHashes = []; - for (let i = 0; i < (witnesses.plutus_scripts()?.len() || 0); i++) { - const script = witnesses.plutus_scripts()!.get(i); - const scriptHash = script.hash(C.ScriptHashNamespace.PlutusV1) - .to_hex(); + const plutusScripts = witnesses.plutus_scripts(); + bucket.push(plutusScripts); + for (let i = 0; i < (plutusScripts?.len() || 0); i++) { + const script = plutusScripts!.get(i); + bucket.push(script); + const hash = script.hash(C.ScriptHashNamespace.PlutusV1); + bucket.push(hash); + const scriptHash = hash.to_hex(); scriptHashes.push(scriptHash); } - for (let i = 0; i < (witnesses.plutus_v2_scripts()?.len() || 0); i++) { - const script = witnesses.plutus_v2_scripts()!.get(i); - const scriptHash = script.hash(C.ScriptHashNamespace.PlutusV2) - .to_hex(); + + const plutusV2Scripts = witnesses.plutus_v2_scripts(); + bucket.push(plutusV2Scripts); + for (let i = 0; i < (plutusV2Scripts?.len() || 0); i++) { + const script = plutusV2Scripts!.get(i); + bucket.push(script); + const hash = script.hash(C.ScriptHashNamespace.PlutusV2); + bucket.push(hash); + const scriptHash = hash.to_hex(); scriptHashes.push(scriptHash); } @@ -380,6 +406,7 @@ export class Emulator implements Provider { })(); const inputs = body.inputs(); + bucket.push(inputs); inputs.sort(); type ResolvedInput = { @@ -392,8 +419,13 @@ export class Emulator implements Provider { // Check existence of inputs and look for script refs. for (let i = 0; i < inputs.len(); i++) { const input = inputs.get(i); + bucket.push(input); + const transactionId = input.transaction_id(); + bucket.push(transactionId); + const transactionIndex = input.index(); + bucket.push(transactionIndex); - const outRef = input.transaction_id().to_hex() + input.index().to_str(); + const outRef = transactionId.to_hex() + transactionIndex.to_str(); const entryLedger = this.ledger[outRef]; @@ -403,12 +435,10 @@ export class Emulator implements Provider { if (!entry || entry.spent) { throw new Error( - `Could not spend UTxO: ${ - JSON.stringify({ - txHash: entry?.utxo.txHash, - outputIndex: entry?.utxo.outputIndex, - }) - }\nIt does not exist or was already spent.`, + `Could not spend UTxO: ${JSON.stringify({ + txHash: entry?.utxo.txHash, + outputIndex: entry?.utxo.outputIndex, + })}\nIt does not exist or was already spent.` ); } @@ -417,23 +447,27 @@ export class Emulator implements Provider { switch (scriptRef.type) { case "Native": { const script = C.NativeScript.from_bytes(fromHex(scriptRef.script)); - nativeHashesOptional[ - script.hash(C.ScriptHashNamespace.NativeScript).to_hex() - ] = script; + bucket.push(script); + const hash = script.hash(C.ScriptHashNamespace.NativeScript); + bucket.push(hash); + + nativeHashesOptional[hash.to_hex()] = script; break; } case "PlutusV1": { const script = C.PlutusScript.from_bytes(fromHex(scriptRef.script)); - plutusHashesOptional.push( - script.hash(C.ScriptHashNamespace.PlutusV1).to_hex(), - ); + bucket.push(script); + const hash = script.hash(C.ScriptHashNamespace.PlutusV1); + bucket.push(hash); + plutusHashesOptional.push(hash.to_hex()); break; } case "PlutusV2": { const script = C.PlutusScript.from_bytes(fromHex(scriptRef.script)); - plutusHashesOptional.push( - script.hash(C.ScriptHashNamespace.PlutusV2).to_hex(), - ); + bucket.push(script); + const hash = script.hash(C.ScriptHashNamespace.PlutusV2); + bucket.push(hash); + plutusHashesOptional.push(hash.to_hex()); break; } } @@ -445,21 +479,27 @@ export class Emulator implements Provider { } // Check existence of reference inputs and look for script refs. - for (let i = 0; i < (body.reference_inputs()?.len() || 0); i++) { - const input = body.reference_inputs()!.get(i); + const referenceInputs = body.reference_inputs(); + bucket.push(referenceInputs); + for (let i = 0; i < (referenceInputs?.len() || 0); i++) { + const input = referenceInputs!.get(i); + bucket.push(input); + + const inputId = input.transaction_id(); + bucket.push(inputId); + const inputIndex = input.index(); + bucket.push(inputIndex); - const outRef = input.transaction_id().to_hex() + input.index().to_str(); + const outRef = inputId.to_hex() + inputIndex.to_str(); const entry = this.ledger[outRef] || this.mempool[outRef]; if (!entry || entry.spent) { throw new Error( - `Could not read UTxO: ${ - JSON.stringify({ - txHash: entry?.utxo.txHash, - outputIndex: entry?.utxo.outputIndex, - }) - }\nIt does not exist or was already spent.`, + `Could not read UTxO: ${JSON.stringify({ + txHash: entry?.utxo.txHash, + outputIndex: entry?.utxo.outputIndex, + })}\nIt does not exist or was already spent.` ); } @@ -468,23 +508,27 @@ export class Emulator implements Provider { switch (scriptRef.type) { case "Native": { const script = C.NativeScript.from_bytes(fromHex(scriptRef.script)); - nativeHashesOptional[ - script.hash(C.ScriptHashNamespace.NativeScript).to_hex() - ] = script; + bucket.push(script); + const hash = script.hash(C.ScriptHashNamespace.NativeScript); + bucket.push(hash); + + nativeHashesOptional[hash.to_hex()] = script; break; } case "PlutusV1": { const script = C.PlutusScript.from_bytes(fromHex(scriptRef.script)); - plutusHashesOptional.push( - script.hash(C.ScriptHashNamespace.PlutusV1).to_hex(), - ); + bucket.push(script); + const hash = script.hash(C.ScriptHashNamespace.PlutusV1); + bucket.push(hash); + plutusHashesOptional.push(hash.to_hex()); break; } case "PlutusV2": { const script = C.PlutusScript.from_bytes(fromHex(scriptRef.script)); - plutusHashesOptional.push( - script.hash(C.ScriptHashNamespace.PlutusV2).to_hex(), - ); + bucket.push(script); + const hash = script.hash(C.ScriptHashNamespace.PlutusV2); + bucket.push(hash); + plutusHashesOptional.push(hash.to_hex()); break; } } @@ -503,11 +547,19 @@ export class Emulator implements Provider { 3: "Reward", }; const collected = []; - for (let i = 0; i < (witnesses.redeemers()?.len() || 0); i++) { - const redeemer = witnesses.redeemers()!.get(i); + const redeemers = witnesses.redeemers(); + bucket.push(redeemers); + for (let i = 0; i < (redeemers?.len() || 0); i++) { + const redeemer = redeemers!.get(i); + bucket.push(redeemer); + const tag = redeemer.tag(); + bucket.push(tag); + + const redeemerIndex = redeemer.index(); + bucket.push(redeemerIndex); collected.push({ - tag: tagMap[redeemer.tag().kind()], - index: parseInt(redeemer.index().to_str()), + tag: tagMap[tag.kind()], + index: parseInt(redeemerIndex.to_str()), }); } return collected; @@ -516,13 +568,13 @@ export class Emulator implements Provider { function checkAndConsumeHash( credential: Credential, tag: Tag | null, - index: number | null, + index: number | null ) { switch (credential.type) { case "Key": { if (!keyHashes.includes(credential.hash)) { throw new Error( - `Missing vkey witness. Key hash: ${credential.hash}`, + `Missing vkey witness. Key hash: ${credential.hash}` ); } consumedHashes.add(credential.hash); @@ -533,19 +585,24 @@ export class Emulator implements Provider { consumedHashes.add(credential.hash); break; } else if (nativeHashesOptional[credential.hash]) { + const lBound = Number.isInteger(lowerBound) + ? C.BigNum.from_str(lowerBound!.toString()) + : undefined; + bucket.push(lBound); + const uBound = Number.isInteger(upperBound) + ? C.BigNum.from_str(upperBound!.toString()) + : undefined; + bucket.push(uBound); + if ( !nativeHashesOptional[credential.hash].verify( - Number.isInteger(lowerBound) - ? C.BigNum.from_str(lowerBound!.toString()) - : undefined, - Number.isInteger(upperBound) - ? C.BigNum.from_str(upperBound!.toString()) - : undefined, - edKeyHashes, + lBound, + uBound, + edKeyHashes ) ) { throw new Error( - `Invalid native script witness. Script hash: ${credential.hash}`, + `Invalid native script witness. Script hash: ${credential.hash}` ); } break; @@ -554,8 +611,8 @@ export class Emulator implements Provider { plutusHashesOptional.includes(credential.hash) ) { if ( - redeemers.find((redeemer) => - redeemer.tag === tag && redeemer.index === index + redeemers.find( + (redeemer) => redeemer.tag === tag && redeemer.index === index ) ) { consumedHashes.add(credential.hash); @@ -563,29 +620,34 @@ export class Emulator implements Provider { } } throw new Error( - `Missing script witness. Script hash: ${credential.hash}`, + `Missing script witness. Script hash: ${credential.hash}` ); } } } // Check collateral inputs + const collateral = body.collateral(); + bucket.push(collateral); + for (let i = 0; i < (collateral?.len() || 0); i++) { + const input = collateral!.get(i); + bucket.push(input); - for (let i = 0; i < (body.collateral()?.len() || 0); i++) { - const input = body.collateral()!.get(i); + const transactionId = input.transaction_id(); + bucket.push(transactionId); + const transactionIndex = input.index(); + bucket.push(transactionIndex); - const outRef = input.transaction_id().to_hex() + input.index().to_str(); + const outRef = transactionId.to_hex() + transactionIndex.to_str(); const entry = this.ledger[outRef] || this.mempool[outRef]; if (!entry || entry.spent) { throw new Error( - `Could not read UTxO: ${ - JSON.stringify({ - txHash: entry?.utxo.txHash, - outputIndex: entry?.utxo.outputIndex, - }) - }\nIt does not exist or was already spent.`, + `Could not read UTxO: ${JSON.stringify({ + txHash: entry?.utxo.txHash, + outputIndex: entry?.utxo.outputIndex, + })}\nIt does not exist or was already spent.` ); } @@ -597,17 +659,24 @@ export class Emulator implements Provider { } // Check required signers - - for (let i = 0; i < (body.required_signers()?.len() || 0); i++) { - const signer = body.required_signers()!.get(i); + const requiredSigners = body.required_signers(); + bucket.push(requiredSigners); + for (let i = 0; i < (requiredSigners?.len() || 0); i++) { + const signer = requiredSigners!.get(i); + bucket.push(signer); checkAndConsumeHash({ type: "Key", hash: signer.to_hex() }, null, null); } // Check mint witnesses - - for (let index = 0; index < (body.mint()?.keys().len() || 0); index++) { - const policyId = body.mint()!.keys().get(index).to_hex(); - checkAndConsumeHash({ type: "Script", hash: policyId }, "Mint", index); + const mint = body.mint(); + bucket.push(mint); + const mintKeys = mint?.keys(); + bucket.push(mintKeys); + for (let index = 0; index < (mintKeys?.len() || 0); index++) { + const policy = mintKeys!.get(index); + bucket.push(policy); + const hash = policy.to_hex(); + checkAndConsumeHash({ type: "Script", hash }, "Mint", index); } // Check withdrawal witnesses @@ -617,23 +686,22 @@ export class Emulator implements Provider { withdrawal: Lovelace; }[] = []; - for ( - let index = 0; - index < (body.withdrawals()?.keys().len() || 0); - index++ - ) { - const rawAddress = body.withdrawals()!.keys().get(index); - const withdrawal: Lovelace = BigInt( - body.withdrawals()!.get(rawAddress)!.to_str(), - ); - const rewardAddress = rawAddress.to_address().to_bech32(undefined); - const { stakeCredential } = getAddressDetails( - rewardAddress, - ); + const withdrawals = body.withdrawals(); + const withdrawalKeys = withdrawals?.keys(); + for (let index = 0; index < (withdrawalKeys?.len() || 0); index++) { + const rawAddress = withdrawalKeys!.get(index); + bucket.push(rawAddress); + const cWithdrawal = withdrawals!.get(rawAddress); + bucket.push(cWithdrawal); + const withdrawal: Lovelace = BigInt(cWithdrawal!.to_str()); + const cAddress = rawAddress.to_address(); + bucket.push(cAddress); + const rewardAddress = cAddress.to_bech32(undefined); + const { stakeCredential } = getAddressDetails(rewardAddress); checkAndConsumeHash(stakeCredential!, "Reward", index); if (this.chain[rewardAddress]?.delegation.rewards !== withdrawal) { throw new Error( - "Withdrawal amount doesn't match actual reward balance.", + "Withdrawal amount doesn't match actual reward balance." ); } withdrawalRequests.push({ rewardAddress, withdrawal }); @@ -647,7 +715,9 @@ export class Emulator implements Provider { poolId?: PoolId; }[] = []; - for (let index = 0; index < (body.certs()?.len() || 0); index++) { + const certs = body.certs(); + bucket.push(certs); + for (let index = 0; index < (certs?.len() || 0); index++) { /* Checking only: 1. Stake registration @@ -656,17 +726,27 @@ export class Emulator implements Provider { All other certificate types are not checked and considered valid. */ - const cert = body.certs()!.get(index); + const cert = certs!.get(index); + bucket.push(cert); switch (cert.kind()) { case 0: { const registration = cert.as_stake_registration()!; - const rewardAddress = C.RewardAddress.new( - C.NetworkInfo.testnet().network_id(), - registration.stake_credential(), - ).to_address().to_bech32(undefined); + bucket.push(registration); + const networkInfo = C.NetworkInfo.testnet(); + bucket.push(networkInfo); + const stakeCredential = registration.stake_credential(); + bucket.push(stakeCredential); + const cRewardAddress = C.RewardAddress.new( + networkInfo.network_id(), + stakeCredential + ); + bucket.push(cRewardAddress); + const address = cRewardAddress.to_address(); + bucket.push(address); + const rewardAddress = address.to_bech32(undefined); if (this.chain[rewardAddress]?.registeredStake) { throw new Error( - `Stake key is already registered. Reward address: ${rewardAddress}`, + `Stake key is already registered. Reward address: ${rewardAddress}` ); } certRequests.push({ type: "Registration", rewardAddress }); @@ -674,17 +754,26 @@ export class Emulator implements Provider { } case 1: { const deregistration = cert.as_stake_deregistration()!; - const rewardAddress = C.RewardAddress.new( - C.NetworkInfo.testnet().network_id(), - deregistration.stake_credential(), - ).to_address().to_bech32(undefined); + bucket.push(deregistration); + const networkInfo = C.NetworkInfo.testnet(); + bucket.push(networkInfo); + const cStakeCredential = deregistration.stake_credential(); + bucket.push(cStakeCredential); + const cRewardAddress = C.RewardAddress.new( + networkInfo.network_id(), + cStakeCredential + ); + bucket.push(cRewardAddress); + const address = cRewardAddress.to_address(); + bucket.push(address); + const rewardAddress = address.to_bech32(undefined); const { stakeCredential } = getAddressDetails(rewardAddress); checkAndConsumeHash(stakeCredential!, "Cert", index); if (!this.chain[rewardAddress]?.registeredStake) { throw new Error( - `Stake key is already deregistered. Reward address: ${rewardAddress}`, + `Stake key is already deregistered. Reward address: ${rewardAddress}` ); } certRequests.push({ type: "Deregistration", rewardAddress }); @@ -692,24 +781,36 @@ export class Emulator implements Provider { } case 2: { const delegation = cert.as_stake_delegation()!; - const rewardAddress = C.RewardAddress.new( - C.NetworkInfo.testnet().network_id(), - delegation.stake_credential(), - ).to_address().to_bech32(undefined); - const poolId = delegation.pool_keyhash().to_bech32("pool"); + bucket.push(delegation); + const networkInfo = C.NetworkInfo.testnet(); + bucket.push(networkInfo); + const cStakeCredential = delegation.stake_credential(); + bucket.push(cStakeCredential); + const cRewardAddress = C.RewardAddress.new( + networkInfo.network_id(), + cStakeCredential + ); + bucket.push(cRewardAddress); + const address = cRewardAddress.to_address(); + bucket.push(address); + const rewardAddress = address.to_bech32(undefined); + const poolKeyHash = delegation.pool_keyhash(); + bucket.push(poolKeyHash); + const poolId = poolKeyHash.to_bech32("pool"); const { stakeCredential } = getAddressDetails(rewardAddress); checkAndConsumeHash(stakeCredential!, "Cert", index); if ( !this.chain[rewardAddress]?.registeredStake && - !certRequests.find((request) => - request.type === "Registration" && - request.rewardAddress === rewardAddress + !certRequests.find( + (request) => + request.type === "Registration" && + request.rewardAddress === rewardAddress ) ) { throw new Error( - `Stake key is not registered. Reward address: ${rewardAddress}`, + `Stake key is not registered. Reward address: ${rewardAddress}` ); } certRequests.push({ type: "Delegation", rewardAddress, poolId }); @@ -727,60 +828,65 @@ export class Emulator implements Provider { // Create outputs and consume datum hashes const outputs = (() => { + const outputs = body.outputs(); + bucket.push(outputs); const collected = []; - for (let i = 0; i < body.outputs().len(); i++) { - const output = body.outputs().get(i); + for (let i = 0; i < outputs.len(); i++) { + const output = outputs.get(i); + bucket.push(output); + const transactionHash = C.TransactionHash.from_hex(txHash); + bucket.push(transactionHash); + const index = C.BigNum.from_str(i.toString()); + bucket.push(index); + const transactionInput = C.TransactionInput.new(transactionHash, index); + bucket.push(transactionInput); const unspentOutput = C.TransactionUnspentOutput.new( - C.TransactionInput.new( - C.TransactionHash.from_hex(txHash), - C.BigNum.from_str(i.toString()), - ), - output, + transactionInput, + output ); + bucket.push(unspentOutput); const utxo = coreToUtxo(unspentOutput); if (utxo.datumHash) consumedHashes.add(utxo.datumHash); - collected.push( - { - utxo, - spent: false, - }, - ); + collected.push({ + utxo, + spent: false, + }); } return collected; })(); // Check consumed witnesses - const [extraKeyHash] = keyHashes.filter((keyHash) => - !consumedHashes.has(keyHash) + const [extraKeyHash] = keyHashes.filter( + (keyHash) => !consumedHashes.has(keyHash) ); if (extraKeyHash) { throw new Error(`Extraneous vkey witness. Key hash: ${extraKeyHash}`); } - const [extraNativeHash] = nativeHashes.filter((scriptHash) => - !consumedHashes.has(scriptHash) + const [extraNativeHash] = nativeHashes.filter( + (scriptHash) => !consumedHashes.has(scriptHash) ); if (extraNativeHash) { throw new Error( - `Extraneous native script. Script hash: ${extraNativeHash}`, + `Extraneous native script. Script hash: ${extraNativeHash}` ); } - const [extraPlutusHash] = plutusHashes.filter((scriptHash) => - !consumedHashes.has(scriptHash) + const [extraPlutusHash] = plutusHashes.filter( + (scriptHash) => !consumedHashes.has(scriptHash) ); if (extraPlutusHash) { throw new Error( - `Extraneous plutus script. Script hash: ${extraPlutusHash}`, + `Extraneous plutus script. Script hash: ${extraPlutusHash}` ); } - const [extraDatumHash] = Object.keys(datumTable).filter((datumHash) => - !consumedHashes.has(datumHash) + const [extraDatumHash] = Object.keys(datumTable).filter( + (datumHash) => !consumedHashes.has(datumHash) ); if (extraDatumHash) { throw new Error(`Extraneous plutus data. Datum hash: ${extraDatumHash}`); @@ -888,21 +994,15 @@ export class Emulator implements Provider { "color:white", "color:yellow", "color:white", - "color:yellow", + "color:yellow" ); console.log("\n"); for (const [address, assets] of Object.entries(balances)) { - console.log( - `Address: %c${address}`, - "color:blue", - "\n", - ); + console.log(`Address: %c${address}`, "color:blue", "\n"); for (const [unit, quantity] of Object.entries(assets)) { const barLength = Math.max( - Math.floor( - 60 * (Number(quantity) / Number(totalBalances[unit])), - ), - 1, + Math.floor(60 * (Number(quantity) / Number(totalBalances[unit]))), + 1 ); console.log( `%c${"\u2586".repeat(barLength) + " ".repeat(60 - barLength)}`, @@ -910,12 +1010,10 @@ export class Emulator implements Provider { "", `${unit}:`, quantity, - "", + "" ); } - console.log( - `\n${"\u2581".repeat(60)}\n`, - ); + console.log(`\n${"\u2581".repeat(60)}\n`); } } } From 7746b77919e89ff71e5a943cc552ead1576803c0 Mon Sep 17 00:00:00 2001 From: yHSJ Date: Sat, 4 Nov 2023 16:41:57 -0400 Subject: [PATCH 36/54] feat: manage memory in maestro.getUtxosInternal --- src/provider/maestro.ts | 103 +++++++++++++++++++++++----------------- 1 file changed, 59 insertions(+), 44 deletions(-) diff --git a/src/provider/maestro.ts b/src/provider/maestro.ts index 26f37113..8b1f16b2 100644 --- a/src/provider/maestro.ts +++ b/src/provider/maestro.ts @@ -46,22 +46,26 @@ export class Maestro implements Provider { // Decimal numbers in Maestro are given as ratio of two numbers represented by string of format "firstNumber/secondNumber". const decimalFromRationalString = (str: string): number => { const forwardSlashIndex = str.indexOf("/"); - return parseInt(str.slice(0, forwardSlashIndex)) / - parseInt(str.slice(forwardSlashIndex + 1)); + return ( + parseInt(str.slice(0, forwardSlashIndex)) / + parseInt(str.slice(forwardSlashIndex + 1)) + ); }; // To rename keys in an object by the given key-map. // deno-lint-ignore no-explicit-any const renameKeysAndSort = (obj: any, newKeys: any) => { - const entries = Object.keys(obj).sort().map((key) => { - const newKey = newKeys[key] || key; - return { - [newKey]: Object.fromEntries( - Object.entries(obj[key]).sort(([k, _v], [k2, _v2]) => - k.localeCompare(k2) + const entries = Object.keys(obj) + .sort() + .map((key) => { + const newKey = newKeys[key] || key; + return { + [newKey]: Object.fromEntries( + Object.entries(obj[key]).sort(([k, _v], [k2, _v2]) => + k.localeCompare(k2) + ) ), - ), - }; - }); + }; + }); return Object.assign({}, ...entries); }; return { @@ -87,20 +91,24 @@ export class Maestro implements Provider { private async getUtxosInternal( addressOrCredential: Address | Credential, - unit?: Unit, + unit?: Unit ): Promise { const queryPredicate = (() => { if (typeof addressOrCredential === "string") { return "/addresses/" + addressOrCredential; } + const hash = + addressOrCredential.type == "Key" + ? C.Ed25519KeyHash.from_hex(addressOrCredential.hash) + : C.ScriptHash.from_hex(addressOrCredential.hash); let credentialBech32Query = "/addresses/cred/"; - credentialBech32Query += addressOrCredential.type === "Key" - ? C.Ed25519KeyHash.from_hex(addressOrCredential.hash).to_bech32( - "addr_vkh", - ) - : C.ScriptHash.from_hex(addressOrCredential.hash).to_bech32( - "addr_shared_vkh", - ); + credentialBech32Query += + addressOrCredential.type === "Key" + ? hash.to_bech32("addr_vkh") + : hash.to_bech32("addr_shared_vkh"); + + hash.free(); + return credentialBech32Query; })(); const qparams = new URLSearchParams({ @@ -112,7 +120,7 @@ export class Maestro implements Provider { await fetch(qry, { headers: this.commonHeaders() }), `${this.url}${queryPredicate}/utxos`, qparams, - "Location: getUtxosInternal. Error: Could not fetch UTxOs from Maestro", + "Location: getUtxosInternal. Error: Could not fetch UTxOs from Maestro" ); return result.map(this.maestroUtxoToUtxo); } @@ -123,7 +131,7 @@ export class Maestro implements Provider { getUtxosWithUnit( addressOrCredential: Address | Credential, - unit: Unit, + unit: Unit ): Promise { return this.getUtxosInternal(addressOrCredential, unit); } @@ -131,7 +139,7 @@ export class Maestro implements Provider { async getUtxoByUnit(unit: Unit): Promise { const timestampedAddressesResponse = await fetch( `${this.url}/assets/${unit}/addresses?count=2`, - { headers: this.commonHeaders() }, + { headers: this.commonHeaders() } ); const timestampedAddresses = await timestampedAddressesResponse.json(); if (!timestampedAddressesResponse.ok) { @@ -140,7 +148,7 @@ export class Maestro implements Provider { } throw new Error( "Location: getUtxoByUnit. Error: Couldn't perform query. Received status code: " + - timestampedAddressesResponse.status, + timestampedAddressesResponse.status ); } const addressesWithAmount = timestampedAddresses.data; @@ -149,7 +157,7 @@ export class Maestro implements Provider { } if (addressesWithAmount.length > 1) { throw new Error( - "Location: getUtxoByUnit. Error: Unit needs to be an NFT or only held by one address.", + "Location: getUtxoByUnit. Error: Unit needs to be an NFT or only held by one address." ); } @@ -159,7 +167,7 @@ export class Maestro implements Provider { if (utxos.length > 1) { throw new Error( - "Location: getUtxoByUnit. Error: Unit needs to be an NFT or only held by one address.", + "Location: getUtxoByUnit. Error: Unit needs to be an NFT or only held by one address." ); } @@ -169,7 +177,7 @@ export class Maestro implements Provider { async getUtxosByOutRef(outRefs: OutRef[]): Promise { const qry = `${this.url}/transactions/outputs`; const body = JSON.stringify( - outRefs.map(({ txHash, outputIndex }) => `${txHash}#${outputIndex}`), + outRefs.map(({ txHash, outputIndex }) => `${txHash}#${outputIndex}`) ); const utxos = await this.getAllPagesData( async (qry: string) => @@ -183,7 +191,7 @@ export class Maestro implements Provider { }), qry, new URLSearchParams({}), - "Location: getUtxosByOutRef. Error: Could not fetch UTxOs by references from Maestro", + "Location: getUtxosByOutRef. Error: Could not fetch UTxOs by references from Maestro" ); return utxos.map(this.maestroUtxoToUtxo); } @@ -191,7 +199,7 @@ export class Maestro implements Provider { async getDelegation(rewardAddress: RewardAddress): Promise { const timestampedResultResponse = await fetch( `${this.url}/accounts/${rewardAddress}`, - { headers: this.commonHeaders() }, + { headers: this.commonHeaders() } ); if (!timestampedResultResponse.ok) { return { poolId: null, rewards: 0n }; @@ -209,7 +217,7 @@ export class Maestro implements Provider { `${this.url}/datum/${datumHash}`, { headers: this.commonHeaders(), - }, + } ); if (!timestampedResultResponse.ok) { if (timestampedResultResponse.status === 404) { @@ -217,7 +225,7 @@ export class Maestro implements Provider { } else { throw new Error( "Location: getDatum. Error: Couldn't successfully perform query. Received status code: " + - timestampedResultResponse.status, + timestampedResultResponse.status ); } } @@ -232,7 +240,7 @@ export class Maestro implements Provider { `${this.url}/transactions/${txHash}/cbor`, { headers: this.commonHeaders(), - }, + } ); if (isConfirmedResponse.ok) { await isConfirmedResponse.json(); @@ -251,7 +259,7 @@ export class Maestro implements Provider { method: "POST", headers: { "Content-Type": "application/cbor", - "Accept": "text/plain", + Accept: "text/plain", ...this.commonHeaders(), }, body: fromHex(tx), @@ -259,10 +267,12 @@ export class Maestro implements Provider { const result = await response.text(); if (!response.ok) { if (response.status === 400) throw new Error(result); - else {throw new Error( + else { + throw new Error( "Could not submit transaction. Received status code: " + - response.status, - );} + response.status + ); + } } return result; } @@ -284,16 +294,21 @@ export class Maestro implements Provider { })(), address: result.address, datumHash: result.datum - ? result.datum.type == "inline" ? undefined : result.datum.hash + ? result.datum.type == "inline" + ? undefined + : result.datum.hash : undefined, datum: result.datum?.bytes, scriptRef: result.reference_script - ? result.reference_script.type == "native" ? undefined : { - type: result.reference_script.type == "plutusv1" - ? "PlutusV1" - : "PlutusV2", - script: applyDoubleCborEncoding(result.reference_script.bytes!), - } + ? result.reference_script.type == "native" + ? undefined + : { + type: + result.reference_script.type == "plutusv1" + ? "PlutusV1" + : "PlutusV2", + script: applyDoubleCborEncoding(result.reference_script.bytes!), + } : undefined, }; } @@ -301,7 +316,7 @@ export class Maestro implements Provider { getResponse: (qry: string) => Promise, qry: string, paramsGiven: URLSearchParams, - errorMsg: string, + errorMsg: string ): Promise> { let nextCursor = null; let result: Array = []; @@ -313,7 +328,7 @@ export class Maestro implements Provider { const pageResult = await response.json(); if (!response.ok) { throw new Error( - `${errorMsg}. Received status code: ${response.status}`, + `${errorMsg}. Received status code: ${response.status}` ); } nextCursor = pageResult.next_cursor; From 46551df8520622e42ecaf18619f065f68a9208c8 Mon Sep 17 00:00:00 2001 From: yHSJ Date: Sat, 4 Nov 2023 16:46:04 -0400 Subject: [PATCH 37/54] feat: manage memory in blockfrost.getUtxos --- src/provider/blockfrost.ts | 156 +++++++++++++++++++------------------ 1 file changed, 79 insertions(+), 77 deletions(-) diff --git a/src/provider/blockfrost.ts b/src/provider/blockfrost.ts index b0991796..ac5cd4e4 100644 --- a/src/provider/blockfrost.ts +++ b/src/provider/blockfrost.ts @@ -52,13 +52,13 @@ export class Blockfrost implements Provider { async getUtxos(addressOrCredential: Address | Credential): Promise { const queryPredicate = (() => { if (typeof addressOrCredential === "string") return addressOrCredential; - const credentialBech32 = addressOrCredential.type === "Key" - ? C.Ed25519KeyHash.from_hex(addressOrCredential.hash).to_bech32( - "addr_vkh", - ) - : C.ScriptHash.from_hex(addressOrCredential.hash).to_bech32( - "addr_vkh", - ); // should be 'script' (CIP-0005) + const hash = + addressOrCredential.type === "Key" + ? C.Ed25519KeyHash.from_hex(addressOrCredential.hash) + : C.ScriptHash.from_hex(addressOrCredential.hash); + + const credentialBech32 = hash.to_bech32("addr_vkh"); // should be 'script' according to CIP-0005, but to maintain bakcwards compatabiltiy I am not changing this + hash.free(); return credentialBech32; })(); let result: BlockfrostUtxoResult = []; @@ -67,7 +67,7 @@ export class Blockfrost implements Provider { const pageResult: BlockfrostUtxoResult | BlockfrostUtxoError = await fetch( `${this.url}/addresses/${queryPredicate}/utxos?page=${page}`, - { headers: { project_id: this.projectId, lucid } }, + { headers: { project_id: this.projectId, lucid } } ).then((res) => res.json()); if ((pageResult as BlockfrostUtxoError).error) { if ((pageResult as BlockfrostUtxoError).status_code === 404) { @@ -86,17 +86,18 @@ export class Blockfrost implements Provider { async getUtxosWithUnit( addressOrCredential: Address | Credential, - unit: Unit, + unit: Unit ): Promise { const queryPredicate = (() => { if (typeof addressOrCredential === "string") return addressOrCredential; - const credentialBech32 = addressOrCredential.type === "Key" - ? C.Ed25519KeyHash.from_hex(addressOrCredential.hash).to_bech32( - "addr_vkh", - ) - : C.ScriptHash.from_hex(addressOrCredential.hash).to_bech32( - "addr_vkh", - ); // should be 'script' (CIP-0005) + const credentialBech32 = + addressOrCredential.type === "Key" + ? C.Ed25519KeyHash.from_hex(addressOrCredential.hash).to_bech32( + "addr_vkh" + ) + : C.ScriptHash.from_hex(addressOrCredential.hash).to_bech32( + "addr_vkh" + ); // should be 'script' (CIP-0005) return credentialBech32; })(); let result: BlockfrostUtxoResult = []; @@ -105,7 +106,7 @@ export class Blockfrost implements Provider { const pageResult: BlockfrostUtxoResult | BlockfrostUtxoError = await fetch( `${this.url}/addresses/${queryPredicate}/utxos/${unit}?page=${page}`, - { headers: { project_id: this.projectId, lucid } }, + { headers: { project_id: this.projectId, lucid } } ).then((res) => res.json()); if ((pageResult as BlockfrostUtxoError).error) { if ((pageResult as BlockfrostUtxoError).status_code === 404) { @@ -125,7 +126,7 @@ export class Blockfrost implements Provider { async getUtxoByUnit(unit: Unit): Promise { const addresses = await fetch( `${this.url}/assets/${unit}/addresses?count=2`, - { headers: { project_id: this.projectId, lucid } }, + { headers: { project_id: this.projectId, lucid } } ).then((res) => res.json()); if (!addresses || addresses.error) { @@ -149,36 +150,42 @@ export class Blockfrost implements Provider { async getUtxosByOutRef(outRefs: OutRef[]): Promise { // TODO: Make sure old already spent UTxOs are not retrievable. const queryHashes = [...new Set(outRefs.map((outRef) => outRef.txHash))]; - const utxos = await Promise.all(queryHashes.map(async (txHash) => { - const result = await fetch( - `${this.url}/txs/${txHash}/utxos`, - { headers: { project_id: this.projectId, lucid } }, - ).then((res) => res.json()); - if (!result || result.error) { - return []; - } - const utxosResult: BlockfrostUtxoResult = result.outputs.map(( - // deno-lint-ignore no-explicit-any - r: any, - ) => ({ - ...r, - tx_hash: txHash, - })); - return this.blockfrostUtxosToUtxos(utxosResult); - })); - - return utxos.reduce((acc, utxos) => acc.concat(utxos), []).filter((utxo) => - outRefs.some((outRef) => - utxo.txHash === outRef.txHash && utxo.outputIndex === outRef.outputIndex - ) + const utxos = await Promise.all( + queryHashes.map(async (txHash) => { + const result = await fetch(`${this.url}/txs/${txHash}/utxos`, { + headers: { project_id: this.projectId, lucid }, + }).then((res) => res.json()); + if (!result || result.error) { + return []; + } + const utxosResult: BlockfrostUtxoResult = result.outputs.map( + ( + // deno-lint-ignore no-explicit-any + r: any + ) => ({ + ...r, + tx_hash: txHash, + }) + ); + return this.blockfrostUtxosToUtxos(utxosResult); + }) ); + + return utxos + .reduce((acc, utxos) => acc.concat(utxos), []) + .filter((utxo) => + outRefs.some( + (outRef) => + utxo.txHash === outRef.txHash && + utxo.outputIndex === outRef.outputIndex + ) + ); } async getDelegation(rewardAddress: RewardAddress): Promise { - const result = await fetch( - `${this.url}/accounts/${rewardAddress}`, - { headers: { project_id: this.projectId, lucid } }, - ).then((res) => res.json()); + const result = await fetch(`${this.url}/accounts/${rewardAddress}`, { + headers: { project_id: this.projectId, lucid }, + }).then((res) => res.json()); if (!result || result.error) { return { poolId: null, rewards: 0n }; } @@ -189,12 +196,9 @@ export class Blockfrost implements Provider { } async getDatum(datumHash: DatumHash): Promise { - const datum = await fetch( - `${this.url}/scripts/datum/${datumHash}/cbor`, - { - headers: { project_id: this.projectId, lucid }, - }, - ) + const datum = await fetch(`${this.url}/scripts/datum/${datumHash}/cbor`, { + headers: { project_id: this.projectId, lucid }, + }) .then((res) => res.json()) .then((res) => res.cbor); if (!datum || datum.error) { @@ -236,43 +240,41 @@ export class Blockfrost implements Provider { } private async blockfrostUtxosToUtxos( - result: BlockfrostUtxoResult, + result: BlockfrostUtxoResult ): Promise { return (await Promise.all( result.map(async (r) => ({ txHash: r.tx_hash, outputIndex: r.output_index, assets: Object.fromEntries( - r.amount.map(({ unit, quantity }) => [unit, BigInt(quantity)]), + r.amount.map(({ unit, quantity }) => [unit, BigInt(quantity)]) ), address: r.address, datumHash: (!r.inline_datum && r.data_hash) || undefined, datum: r.inline_datum || undefined, scriptRef: r.reference_script_hash - ? (await (async () => { - const { - type, - } = await fetch( - `${this.url}/scripts/${r.reference_script_hash}`, - { - headers: { project_id: this.projectId, lucid }, - }, - ).then((res) => res.json()); - // TODO: support native scripts - if (type === "Native" || type === "native") { - throw new Error("Native script ref not implemented!"); - } - const { cbor: script } = await fetch( - `${this.url}/scripts/${r.reference_script_hash}/cbor`, - { headers: { project_id: this.projectId, lucid } }, - ).then((res) => res.json()); - return { - type: type === "plutusV1" ? "PlutusV1" : "PlutusV2", - script: applyDoubleCborEncoding(script), - }; - })()) + ? await (async () => { + const { type } = await fetch( + `${this.url}/scripts/${r.reference_script_hash}`, + { + headers: { project_id: this.projectId, lucid }, + } + ).then((res) => res.json()); + // TODO: support native scripts + if (type === "Native" || type === "native") { + throw new Error("Native script ref not implemented!"); + } + const { cbor: script } = await fetch( + `${this.url}/scripts/${r.reference_script_hash}/cbor`, + { headers: { project_id: this.projectId, lucid } } + ).then((res) => res.json()); + return { + type: type === "plutusV1" ? "PlutusV1" : "PlutusV2", + script: applyDoubleCborEncoding(script), + }; + })() : undefined, - })), + })) )) as UTxO[]; } } @@ -307,8 +309,8 @@ export function datumJsonToCbor(json: DatumJson): Datum { return C.PlutusData.new_constr_plutus_data( C.ConstrPlutusData.new( C.BigNum.from_str(json.constructor!.toString()), - l, - ), + l + ) ); } throw new Error("Unsupported type"); From 2e7f15d924a373ee246fa8c3ce2bfa05f9b32f4f Mon Sep 17 00:00:00 2001 From: yHSJ Date: Sat, 4 Nov 2023 16:47:18 -0400 Subject: [PATCH 38/54] feat: manage memory in blockfrost.getUtxosWithUnit --- src/provider/blockfrost.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/provider/blockfrost.ts b/src/provider/blockfrost.ts index ac5cd4e4..f4e225f1 100644 --- a/src/provider/blockfrost.ts +++ b/src/provider/blockfrost.ts @@ -90,14 +90,13 @@ export class Blockfrost implements Provider { ): Promise { const queryPredicate = (() => { if (typeof addressOrCredential === "string") return addressOrCredential; - const credentialBech32 = + const hash = addressOrCredential.type === "Key" - ? C.Ed25519KeyHash.from_hex(addressOrCredential.hash).to_bech32( - "addr_vkh" - ) - : C.ScriptHash.from_hex(addressOrCredential.hash).to_bech32( - "addr_vkh" - ); // should be 'script' (CIP-0005) + ? C.Ed25519KeyHash.from_hex(addressOrCredential.hash) + : C.ScriptHash.from_hex(addressOrCredential.hash); + + const credentialBech32 = hash.to_bech32("addr_vkh"); // should be 'script' according to CIP-0005, but to maintain bakcwards compatabiltiy I am not changing this + hash.free(); return credentialBech32; })(); let result: BlockfrostUtxoResult = []; From 78ab324792190bd79b2160073973526b99a487a9 Mon Sep 17 00:00:00 2001 From: yHSJ Date: Sat, 4 Nov 2023 16:51:50 -0400 Subject: [PATCH 39/54] feat: manage memory in blockfrost.datumJsonToCbor --- src/provider/blockfrost.ts | 82 ++++++++++++++++++++++++-------------- 1 file changed, 53 insertions(+), 29 deletions(-) diff --git a/src/provider/blockfrost.ts b/src/provider/blockfrost.ts index f4e225f1..3960dbef 100644 --- a/src/provider/blockfrost.ts +++ b/src/provider/blockfrost.ts @@ -16,6 +16,7 @@ import { UTxO, } from "../types/mod.ts"; import packageJson from "../../package.json" assert { type: "json" }; +import { FreeableBucket, Freeables } from "../utils/freeable.ts"; export class Blockfrost implements Provider { url: string; @@ -284,38 +285,61 @@ export class Blockfrost implements Provider { */ export function datumJsonToCbor(json: DatumJson): Datum { const convert = (json: DatumJson): C.PlutusData => { - if (!isNaN(json.int!)) { - return C.PlutusData.new_integer(C.BigInt.from_str(json.int!.toString())); - } else if (json.bytes || !isNaN(Number(json.bytes))) { - return C.PlutusData.new_bytes(fromHex(json.bytes!)); - } else if (json.map) { - const m = C.PlutusMap.new(); - json.map.forEach(({ k, v }: { k: unknown; v: unknown }) => { - m.insert(convert(k as DatumJson), convert(v as DatumJson)); - }); - return C.PlutusData.new_map(m); - } else if (json.list) { - const l = C.PlutusList.new(); - json.list.forEach((v: DatumJson) => { - l.add(convert(v)); - }); - return C.PlutusData.new_list(l); - } else if (!isNaN(json.constructor! as unknown as number)) { - const l = C.PlutusList.new(); - json.fields!.forEach((v: DatumJson) => { - l.add(convert(v)); - }); - return C.PlutusData.new_constr_plutus_data( - C.ConstrPlutusData.new( - C.BigNum.from_str(json.constructor!.toString()), - l - ) - ); + const bucket: FreeableBucket = []; + try { + if (!isNaN(json.int!)) { + const int = C.BigInt.from_str(json.int!.toString()); + bucket.push(int); + return C.PlutusData.new_integer(int); + } else if (json.bytes || !isNaN(Number(json.bytes))) { + return C.PlutusData.new_bytes(fromHex(json.bytes!)); + } else if (json.map) { + const m = C.PlutusMap.new(); + bucket.push(m); + json.map.forEach(({ k, v }: { k: unknown; v: unknown }) => { + const key = convert(k as DatumJson); + bucket.push(key); + const value = convert(v as DatumJson); + bucket.push(value); + + m.insert(key, value); + }); + return C.PlutusData.new_map(m); + } else if (json.list) { + const l = C.PlutusList.new(); + bucket.push(l); + json.list.forEach((v: DatumJson) => { + const value = convert(v); + bucket.push(value); + l.add(value); + }); + return C.PlutusData.new_list(l); + } else if (!isNaN(json.constructor! as unknown as number)) { + const l = C.PlutusList.new(); + bucket.push(l); + json.fields!.forEach((v: DatumJson) => { + const value = convert(v); + bucket.push(value); + l.add(value); + }); + const constructorIndex = C.BigNum.from_str( + json.constructor!.toString() + ); + bucket.push(constructorIndex); + const plutusData = C.ConstrPlutusData.new(constructorIndex, l); + bucket.push(plutusData); + return C.PlutusData.new_constr_plutus_data(plutusData); + } + throw new Error("Unsupported type"); + } finally { + Freeables.free(...bucket); } - throw new Error("Unsupported type"); }; - return toHex(convert(json).to_bytes()); + const convertedJson = convert(json); + const cbor = convertedJson.to_bytes(); + convertedJson.free(); + return toHex(cbor); } type DatumJson = { From 70197eba8d70405b784fec660b13b716999ae923 Mon Sep 17 00:00:00 2001 From: yHSJ Date: Sat, 4 Nov 2023 16:56:05 -0400 Subject: [PATCH 40/54] feat: mange memory in tx_signed.toHash --- src/lucid/tx_signed.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/lucid/tx_signed.ts b/src/lucid/tx_signed.ts index 5d7bf2b9..885fa013 100644 --- a/src/lucid/tx_signed.ts +++ b/src/lucid/tx_signed.ts @@ -13,7 +13,7 @@ export class TxSigned { async submit(): Promise { return await (this.lucid.wallet || this.lucid.provider).submitTx( - toHex(this.txSigned.to_bytes()), + toHex(this.txSigned.to_bytes()) ); } @@ -24,6 +24,14 @@ export class TxSigned { /** Return the transaction hash. */ toHash(): TxHash { - return C.hash_transaction(this.txSigned.body()).to_hex(); + const hash = C.hash_transaction(this.txSigned.body()); + const txHash = hash.to_hex(); + hash.free(); + return txHash; + } + + /** Since this object has WASM parameters, we must use the free method to free the parameters */ + free() { + this.txSigned.free(); } } From 8a212b758752ca841aa4cc42c55cf9f275c16c8e Mon Sep 17 00:00:00 2001 From: yHSJ Date: Sat, 4 Nov 2023 16:59:52 -0400 Subject: [PATCH 41/54] feat: manage memory in message.signWithPrivateKey --- src/lucid/message.ts | 40 +++++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/src/lucid/message.ts b/src/lucid/message.ts index fb71194c..a43ee0ee 100644 --- a/src/lucid/message.ts +++ b/src/lucid/message.ts @@ -8,6 +8,7 @@ import { } from "../types/mod.ts"; import { signData } from "../misc/sign_data.ts"; import { C } from "../mod.ts"; +import { FreeableBucket, Freeables } from "../utils/freeable.ts"; export class Message { lucid: Lucid; @@ -17,7 +18,7 @@ export class Message { constructor( lucid: Lucid, address: Address | RewardAddress, - payload: Payload, + payload: Payload ) { this.lucid = lucid; this.address = address; @@ -31,18 +32,31 @@ export class Message { /** Sign message with a separate private key. */ signWithPrivateKey(privateKey: PrivateKey): SignedMessage { - const { paymentCredential, stakeCredential, address: { hex: hexAddress } } = - this.lucid.utils.getAddressDetails(this.address); - - const keyHash = paymentCredential?.hash || stakeCredential?.hash; - - const keyHashOriginal = C.PrivateKey.from_bech32(privateKey).to_public() - .hash().to_hex(); - - if (!keyHash || keyHash !== keyHashOriginal) { - throw new Error(`Cannot sign message for address: ${this.address}.`); + const bucket: FreeableBucket = []; + try { + const { + paymentCredential, + stakeCredential, + address: { hex: hexAddress }, + } = this.lucid.utils.getAddressDetails(this.address); + + const keyHash = paymentCredential?.hash || stakeCredential?.hash; + + const skey = C.PrivateKey.from_bech32(privateKey); + bucket.push(skey); + const vkey = skey.to_public(); + bucket.push(vkey); + const hash = vkey.hash(); + bucket.push(hash); + const keyHashOriginal = hash.to_hex(); + + if (!keyHash || keyHash !== keyHashOriginal) { + throw new Error(`Cannot sign message for address: ${this.address}.`); + } + + return signData(hexAddress, this.payload, privateKey); + } finally { + Freeables.free(...bucket); } - - return signData(hexAddress, this.payload, privateKey); } } From 9fb26d31fcc11cca7b99c99eb6fe2301e8993b85 Mon Sep 17 00:00:00 2001 From: yHSJ Date: Sat, 4 Nov 2023 17:06:25 -0400 Subject: [PATCH 42/54] feat: manage memory in TxComplete.constructor --- src/lucid/tx_complete.ts | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/src/lucid/tx_complete.ts b/src/lucid/tx_complete.ts index 1566b4dd..231b6ff7 100644 --- a/src/lucid/tx_complete.ts +++ b/src/lucid/tx_complete.ts @@ -8,6 +8,7 @@ import { import { Lucid } from "./lucid.ts"; import { TxSigned } from "./tx_signed.ts"; import { fromHex, toHex } from "../utils/mod.ts"; +import { FreeableBucket, Freeables } from "../utils/freeable.ts"; export class TxComplete { txComplete: C.Transaction; @@ -18,22 +19,41 @@ export class TxComplete { exUnits: { cpu: number; mem: number } | null = null; constructor(lucid: Lucid, tx: C.Transaction) { + const bucket: FreeableBucket = []; this.lucid = lucid; this.txComplete = tx; this.witnessSetBuilder = C.TransactionWitnessSetBuilder.new(); this.tasks = []; - this.fee = parseInt(tx.body().fee().to_str()); - const redeemers = tx.witness_set().redeemers(); + const body = tx.body(); + bucket.push(body); + const fee = body.fee(); + bucket.push(fee); + const witnessSet = tx.witness_set(); + bucket.push(witnessSet); + + this.fee = parseInt(fee.to_str()); + const redeemers = witnessSet.redeemers(); + bucket.push(redeemers); if (redeemers) { const exUnits = { cpu: 0, mem: 0 }; for (let i = 0; i < redeemers.len(); i++) { const redeemer = redeemers.get(i); - exUnits.cpu += parseInt(redeemer.ex_units().steps().to_str()); - exUnits.mem += parseInt(redeemer.ex_units().mem().to_str()); + bucket.push(redeemer); + const cExUnits = redeemer.ex_units(); + bucket.push(cExUnits); + const steps = cExUnits.steps(); + bucket.push(steps); + const mem = cExUnits.mem(); + bucket.push(mem); + + exUnits.cpu += parseInt(steps.to_str()); + exUnits.mem += parseInt(mem.to_str()); } this.exUnits = exUnits; } + + Freeables.free(...bucket); } sign(): TxComplete { this.tasks.push(async () => { @@ -48,7 +68,7 @@ export class TxComplete { const priv = C.PrivateKey.from_bech32(privateKey); const witness = C.make_vkey_witness( C.hash_transaction(this.txComplete.body()), - priv, + priv ); this.witnessSetBuilder.add_vkey(witness); return this; @@ -69,7 +89,7 @@ export class TxComplete { const priv = C.PrivateKey.from_bech32(privateKey); const witness = C.make_vkey_witness( C.hash_transaction(this.txComplete.body()), - priv, + priv ); this.witnessSetBuilder.add_vkey(witness); const witnesses = C.TransactionWitnessSetBuilder.new(); @@ -81,7 +101,7 @@ export class TxComplete { assemble(witnesses: TransactionWitnesses[]): TxComplete { witnesses.forEach((witness) => { const witnessParsed = C.TransactionWitnessSet.from_bytes( - fromHex(witness), + fromHex(witness) ); this.witnessSetBuilder.add_existing(witnessParsed); }); @@ -97,7 +117,7 @@ export class TxComplete { const signedTx = C.Transaction.new( this.txComplete.body(), this.witnessSetBuilder.build(), - this.txComplete.auxiliary_data(), + this.txComplete.auxiliary_data() ); return new TxSigned(this.lucid, signedTx); } From 7546a41bd97b2c0e55bf7fee051e9517e6974521 Mon Sep 17 00:00:00 2001 From: yHSJ Date: Sat, 4 Nov 2023 17:06:36 -0400 Subject: [PATCH 43/54] feat: manage memory in TxComplete.sign --- src/lucid/tx_complete.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lucid/tx_complete.ts b/src/lucid/tx_complete.ts index 231b6ff7..cb871ccf 100644 --- a/src/lucid/tx_complete.ts +++ b/src/lucid/tx_complete.ts @@ -59,6 +59,7 @@ export class TxComplete { this.tasks.push(async () => { const witnesses = await this.lucid.wallet.signTx(this.txComplete); this.witnessSetBuilder.add_existing(witnesses); + witnesses.free(); }); return this; } From 4b9dd7fdcad8c2a9cd6b191649def841ebcf78d2 Mon Sep 17 00:00:00 2001 From: yHSJ Date: Sat, 4 Nov 2023 17:07:56 -0400 Subject: [PATCH 44/54] feat: manage memory in TxComplete.signWithPrivateKey --- src/lucid/tx_complete.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/lucid/tx_complete.ts b/src/lucid/tx_complete.ts index cb871ccf..9b9b2fe8 100644 --- a/src/lucid/tx_complete.ts +++ b/src/lucid/tx_complete.ts @@ -66,12 +66,17 @@ export class TxComplete { /** Add an extra signature from a private key. */ signWithPrivateKey(privateKey: PrivateKey): TxComplete { + const bucket: FreeableBucket = []; const priv = C.PrivateKey.from_bech32(privateKey); - const witness = C.make_vkey_witness( - C.hash_transaction(this.txComplete.body()), - priv - ); + bucket.push(priv); + const body = this.txComplete.body(); + bucket.push(body); + const hash = C.hash_transaction(body); + bucket.push(hash); + const witness = C.make_vkey_witness(hash, priv); + bucket.push(witness); this.witnessSetBuilder.add_vkey(witness); + Freeables.free(...bucket); return this; } From 5325911092c373506c610a65d6c51d80cc681322 Mon Sep 17 00:00:00 2001 From: yHSJ Date: Sat, 4 Nov 2023 17:08:41 -0400 Subject: [PATCH 45/54] feat: manage memory in TxComplete.partialSign --- src/lucid/tx_complete.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lucid/tx_complete.ts b/src/lucid/tx_complete.ts index 9b9b2fe8..e29a957e 100644 --- a/src/lucid/tx_complete.ts +++ b/src/lucid/tx_complete.ts @@ -84,7 +84,9 @@ export class TxComplete { async partialSign(): Promise { const witnesses = await this.lucid.wallet.signTx(this.txComplete); this.witnessSetBuilder.add_existing(witnesses); - return toHex(witnesses.to_bytes()); + const bytes = witnesses.to_bytes(); + witnesses.free(); + return toHex(bytes); } /** From 8c07f533d8dafa11cb693193d01aba265096095e Mon Sep 17 00:00:00 2001 From: yHSJ Date: Sat, 4 Nov 2023 17:10:16 -0400 Subject: [PATCH 46/54] feat: manage memory in TxComplete.partialSignWithPrivateKey --- src/lucid/tx_complete.ts | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/lucid/tx_complete.ts b/src/lucid/tx_complete.ts index e29a957e..3a29b6b7 100644 --- a/src/lucid/tx_complete.ts +++ b/src/lucid/tx_complete.ts @@ -94,15 +94,26 @@ export class TxComplete { * Add an extra signature from a private key. */ partialSignWithPrivateKey(privateKey: PrivateKey): TransactionWitnesses { + const bucket: FreeableBucket = []; const priv = C.PrivateKey.from_bech32(privateKey); - const witness = C.make_vkey_witness( - C.hash_transaction(this.txComplete.body()), - priv - ); + bucket.push(priv); + const body = this.txComplete.body(); + bucket.push(body); + const hash = C.hash_transaction(body); + bucket.push(hash); + const witness = C.make_vkey_witness(hash, priv); + bucket.push(witness); + this.witnessSetBuilder.add_vkey(witness); const witnesses = C.TransactionWitnessSetBuilder.new(); + bucket.push(witnesses); witnesses.add_vkey(witness); - return toHex(witnesses.build().to_bytes()); + const witnessSet = witnesses.build(); + bucket.push(witnessSet); + const bytes = witnessSet.to_bytes(); + + Freeables.free(...bucket); + return toHex(bytes); } /** Sign the transaction with the given witnesses. */ From 362aadbda112b362807d294d6ce762732bec720a Mon Sep 17 00:00:00 2001 From: yHSJ Date: Sat, 4 Nov 2023 17:10:52 -0400 Subject: [PATCH 47/54] feat: manage memory in TxComplete.assemble --- src/lucid/tx_complete.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lucid/tx_complete.ts b/src/lucid/tx_complete.ts index 3a29b6b7..0c5b2a37 100644 --- a/src/lucid/tx_complete.ts +++ b/src/lucid/tx_complete.ts @@ -123,6 +123,7 @@ export class TxComplete { fromHex(witness) ); this.witnessSetBuilder.add_existing(witnessParsed); + witnessParsed.free(); }); return this; } From 601370306bfa11379babb69ec9859d573b3bb568 Mon Sep 17 00:00:00 2001 From: yHSJ Date: Sat, 4 Nov 2023 17:12:42 -0400 Subject: [PATCH 48/54] feat: manage memory in TxComplete.complete --- src/lucid/tx_complete.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/lucid/tx_complete.ts b/src/lucid/tx_complete.ts index 0c5b2a37..f81fec46 100644 --- a/src/lucid/tx_complete.ts +++ b/src/lucid/tx_complete.ts @@ -133,12 +133,19 @@ export class TxComplete { await task(); } - this.witnessSetBuilder.add_existing(this.txComplete.witness_set()); - const signedTx = C.Transaction.new( - this.txComplete.body(), - this.witnessSetBuilder.build(), - this.txComplete.auxiliary_data() - ); + const bucket: FreeableBucket = []; + const txCompleteWitnessSet = this.txComplete.witness_set(); + bucket.push(txCompleteWitnessSet); + this.witnessSetBuilder.add_existing(txCompleteWitnessSet); + const body = this.txComplete.body(); + bucket.push(body); + const witnessSet = this.witnessSetBuilder.build(); + bucket.push(witnessSet); + const auxiliaryData = this.txComplete.auxiliary_data(); + bucket.push(auxiliaryData); + const signedTx = C.Transaction.new(body, witnessSet, auxiliaryData); + + Freeables.free(...bucket); return new TxSigned(this.lucid, signedTx); } From 5941c9ac771bb8df92615743685e983dbc517c95 Mon Sep 17 00:00:00 2001 From: yHSJ Date: Sat, 4 Nov 2023 17:13:16 -0400 Subject: [PATCH 49/54] feat: manage memory in TxComplete.toHash --- src/lucid/tx_complete.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/lucid/tx_complete.ts b/src/lucid/tx_complete.ts index f81fec46..86d66b41 100644 --- a/src/lucid/tx_complete.ts +++ b/src/lucid/tx_complete.ts @@ -156,6 +156,10 @@ export class TxComplete { /** Return the transaction hash. */ toHash(): TxHash { - return C.hash_transaction(this.txComplete.body()).to_hex(); + const body = this.txComplete.body(); + const hash = C.hash_transaction(body); + const txHash = hash.to_hex(); + Freeables.free(body, hash); + return txHash; } } From a2a139ecd0809af25308474dc96d52974c33b235 Mon Sep 17 00:00:00 2001 From: yHSJ Date: Sat, 4 Nov 2023 17:13:41 -0400 Subject: [PATCH 50/54] feat: add free method to TxComplete --- src/lucid/tx_complete.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/lucid/tx_complete.ts b/src/lucid/tx_complete.ts index 86d66b41..981e7ea6 100644 --- a/src/lucid/tx_complete.ts +++ b/src/lucid/tx_complete.ts @@ -162,4 +162,10 @@ export class TxComplete { Freeables.free(body, hash); return txHash; } + + /** Since this object has WASM parameters, we must use the free method to free the parameters */ + free() { + this.txComplete.free(); + this.witnessSetBuilder.free(); + } } From 1fe855782127660ff7fb6b66f01bbbc1cd26e748 Mon Sep 17 00:00:00 2001 From: yHSJ Date: Sat, 4 Nov 2023 17:18:44 -0400 Subject: [PATCH 51/54] feat manage memory in kupmios.kupmiosUtxosToUtxos --- src/provider/kupmios.ts | 267 ++++++++++++++++++++++------------------ 1 file changed, 144 insertions(+), 123 deletions(-) diff --git a/src/provider/kupmios.ts b/src/provider/kupmios.ts index 06214db7..b4dd4ecd 100644 --- a/src/provider/kupmios.ts +++ b/src/provider/kupmios.ts @@ -36,22 +36,23 @@ export class Kupmios implements Provider { }); return new Promise((res, rej) => { - client.addEventListener("message", (msg: MessageEvent) => { - try { - const { result } = JSON.parse(msg.data); + client.addEventListener( + "message", + (msg: MessageEvent) => { + try { + const { result } = JSON.parse(msg.data); - // deno-lint-ignore no-explicit-any - const costModels: any = {}; - Object.keys(result.costModels).forEach((v) => { - const version = v.split(":")[1].toUpperCase(); - const plutusVersion = "Plutus" + version; - costModels[plutusVersion] = result.costModels[v]; - }); - const [memNum, memDenom] = result.prices.memory.split("/"); - const [stepsNum, stepsDenom] = result.prices.steps.split("/"); + // deno-lint-ignore no-explicit-any + const costModels: any = {}; + Object.keys(result.costModels).forEach((v) => { + const version = v.split(":")[1].toUpperCase(); + const plutusVersion = "Plutus" + version; + costModels[plutusVersion] = result.costModels[v]; + }); + const [memNum, memDenom] = result.prices.memory.split("/"); + const [stepsNum, stepsDenom] = result.prices.steps.split("/"); - res( - { + res({ minFeeA: parseInt(result.minFeeCoefficient), minFeeB: parseInt(result.minFeeConstant), maxTxSize: parseInt(result.maxTxSize), @@ -62,19 +63,20 @@ export class Kupmios implements Provider { priceStep: parseInt(stepsNum) / parseInt(stepsDenom), maxTxExMem: BigInt(result.maxExecutionUnitsPerTransaction.memory), maxTxExSteps: BigInt( - result.maxExecutionUnitsPerTransaction.steps, + result.maxExecutionUnitsPerTransaction.steps ), coinsPerUtxoByte: BigInt(result.coinsPerUtxoByte), collateralPercentage: parseInt(result.collateralPercentage), maxCollateralInputs: parseInt(result.maxCollateralInputs), costModels, - }, - ); - client.close(); - } catch (e) { - rej(e); - } - }, { once: true }); + }); + client.close(); + } catch (e) { + rej(e); + } + }, + { once: true } + ); }); } @@ -86,15 +88,14 @@ export class Kupmios implements Provider { const result = await fetch( `${this.kupoUrl}/matches/${queryPredicate}${ isAddress ? "" : "/*" - }?unspent`, - ) - .then((res) => res.json()); + }?unspent` + ).then((res) => res.json()); return this.kupmiosUtxosToUtxos(result); } async getUtxosWithUnit( addressOrCredential: Address | Credential, - unit: Unit, + unit: Unit ): Promise { const isAddress = typeof addressOrCredential === "string"; const queryPredicate = isAddress @@ -106,9 +107,8 @@ export class Kupmios implements Provider { isAddress ? "" : "/*" }?unspent&policy_id=${policyId}${ assetName ? `&asset_name=${assetName}` : "" - }`, - ) - .then((res) => res.json()); + }` + ).then((res) => res.json()); return this.kupmiosUtxosToUtxos(result); } @@ -117,9 +117,8 @@ export class Kupmios implements Provider { const result = await fetch( `${this.kupoUrl}/matches/${policyId}.${ assetName ? `${assetName}` : "*" - }?unspent`, - ) - .then((res) => res.json()); + }?unspent` + ).then((res) => res.json()); const utxos = await this.kupmiosUtxosToUtxos(result); @@ -133,51 +132,59 @@ export class Kupmios implements Provider { async getUtxosByOutRef(outRefs: Array): Promise { const queryHashes = [...new Set(outRefs.map((outRef) => outRef.txHash))]; - const utxos = await Promise.all(queryHashes.map(async (txHash) => { - const result = await fetch( - `${this.kupoUrl}/matches/*@${txHash}?unspent`, - ).then((res) => res.json()); - return this.kupmiosUtxosToUtxos(result); - })); - - return utxos.reduce((acc, utxos) => acc.concat(utxos), []).filter((utxo) => - outRefs.some((outRef) => - utxo.txHash === outRef.txHash && utxo.outputIndex === outRef.outputIndex - ) + const utxos = await Promise.all( + queryHashes.map(async (txHash) => { + const result = await fetch( + `${this.kupoUrl}/matches/*@${txHash}?unspent` + ).then((res) => res.json()); + return this.kupmiosUtxosToUtxos(result); + }) ); + + return utxos + .reduce((acc, utxos) => acc.concat(utxos), []) + .filter((utxo) => + outRefs.some( + (outRef) => + utxo.txHash === outRef.txHash && + utxo.outputIndex === outRef.outputIndex + ) + ); } async getDelegation(rewardAddress: RewardAddress): Promise { const client = await this.ogmiosWsp("Query", { - query: { "delegationsAndRewards": [rewardAddress] }, + query: { delegationsAndRewards: [rewardAddress] }, }); return new Promise((res, rej) => { - client.addEventListener("message", (msg: MessageEvent) => { - try { - const { result } = JSON.parse(msg.data); - const delegation = (result ? Object.values(result)[0] : {}) as { - delegate: string; - rewards: number; - }; - res( - { + client.addEventListener( + "message", + (msg: MessageEvent) => { + try { + const { result } = JSON.parse(msg.data); + const delegation = (result ? Object.values(result)[0] : {}) as { + delegate: string; + rewards: number; + }; + res({ poolId: delegation?.delegate || null, rewards: BigInt(delegation?.rewards || 0), - }, - ); - client.close(); - } catch (e) { - rej(e); - } - }, { once: true }); + }); + client.close(); + } catch (e) { + rej(e); + } + }, + { once: true } + ); }); } async getDatum(datumHash: DatumHash): Promise { - const result = await fetch( - `${this.kupoUrl}/datums/${datumHash}`, - ).then((res) => res.json()); + const result = await fetch(`${this.kupoUrl}/datums/${datumHash}`).then( + (res) => res.json() + ); if (!result || !result.datum) { throw new Error(`No datum found for datum hash: ${datumHash}`); } @@ -188,7 +195,7 @@ export class Kupmios implements Provider { return new Promise((res) => { const confirmation = setInterval(async () => { const isConfirmed = await fetch( - `${this.kupoUrl}/matches/*@${txHash}?unspent`, + `${this.kupoUrl}/matches/*@${txHash}?unspent` ).then((res) => res.json()); if (isConfirmed && isConfirmed.length > 0) { clearInterval(confirmation); @@ -205,80 +212,94 @@ export class Kupmios implements Provider { }); return new Promise((res, rej) => { - client.addEventListener("message", (msg: MessageEvent) => { - try { - const { result } = JSON.parse(msg.data); + client.addEventListener( + "message", + (msg: MessageEvent) => { + try { + const { result } = JSON.parse(msg.data); - if (result.SubmitSuccess) res(result.SubmitSuccess.txId); - else rej(result.SubmitFail); - client.close(); - } catch (e) { - rej(e); - } - }, { once: true }); + if (result.SubmitSuccess) res(result.SubmitSuccess.txId); + else rej(result.SubmitFail); + client.close(); + } catch (e) { + rej(e); + } + }, + { once: true } + ); }); } private kupmiosUtxosToUtxos(utxos: unknown): Promise { - // deno-lint-ignore no-explicit-any - return Promise.all((utxos as any).map(async (utxo: any) => { - return ({ - txHash: utxo.transaction_id, - outputIndex: parseInt(utxo.output_index), - address: utxo.address, - assets: (() => { - const a: Assets = { lovelace: BigInt(utxo.value.coins) }; - Object.keys(utxo.value.assets).forEach((unit) => { - a[unit.replace(".", "")] = BigInt(utxo.value.assets[unit]); - }); - return a; - })(), - datumHash: utxo?.datum_type === "hash" ? utxo.datum_hash : null, - datum: utxo?.datum_type === "inline" - ? await this.getDatum(utxo.datum_hash) - : null, - scriptRef: utxo.script_hash && - (await (async () => { - const { - script, - language, - } = await fetch( - `${this.kupoUrl}/scripts/${utxo.script_hash}`, - ).then((res) => res.json()); + return Promise.all( + // deno-lint-ignore no-explicit-any + (utxos as any).map(async (utxo: any) => { + return { + txHash: utxo.transaction_id, + outputIndex: parseInt(utxo.output_index), + address: utxo.address, + assets: (() => { + const a: Assets = { lovelace: BigInt(utxo.value.coins) }; + Object.keys(utxo.value.assets).forEach((unit) => { + a[unit.replace(".", "")] = BigInt(utxo.value.assets[unit]); + }); + return a; + })(), + datumHash: utxo?.datum_type === "hash" ? utxo.datum_hash : null, + datum: + utxo?.datum_type === "inline" + ? await this.getDatum(utxo.datum_hash) + : null, + scriptRef: + utxo.script_hash && + (await (async () => { + const { script, language } = await fetch( + `${this.kupoUrl}/scripts/${utxo.script_hash}` + ).then((res) => res.json()); - if (language === "native") { - return { type: "Native", script }; - } else if (language === "plutus:v1") { - return { - type: "PlutusV1", - script: toHex(C.PlutusScript.new(fromHex(script)).to_bytes()), - }; - } else if (language === "plutus:v2") { - return { - type: "PlutusV2", - script: toHex(C.PlutusScript.new(fromHex(script)).to_bytes()), - }; - } - })()), - }) as UTxO; - })); + if (language === "native") { + return { type: "Native", script }; + } else if (language === "plutus:v1") { + const plutusScript = C.PlutusScript.new(fromHex(script)); + const scriptBytes = plutusScript.to_bytes(); + plutusScript.free(); + return { + type: "PlutusV1", + script: toHex(scriptBytes), + }; + } else if (language === "plutus:v2") { + const plutusScript = C.PlutusScript.new(fromHex(script)); + const scriptBytes = plutusScript.to_bytes(); + plutusScript.free(); + + return { + type: "PlutusV2", + script: toHex(scriptBytes), + }; + } + })()), + } as UTxO; + }) + ); } private async ogmiosWsp( methodname: string, - args: unknown, + args: unknown ): Promise { const client = new WebSocket(this.ogmiosUrl); await new Promise((res) => { client.addEventListener("open", () => res(1), { once: true }); }); - client.send(JSON.stringify({ - type: "jsonwsp/request", - version: "1.0", - servicename: "ogmios", - methodname, - args, - })); + client.send( + JSON.stringify({ + type: "jsonwsp/request", + version: "1.0", + servicename: "ogmios", + methodname, + args, + }) + ); return client; } } From 0bc585867f94bff48e5bd78e2243a44d87478556 Mon Sep 17 00:00:00 2001 From: yHSJ Date: Sat, 4 Nov 2023 17:27:53 -0400 Subject: [PATCH 52/54] feat: add memory management to pool_registration example --- src/examples/pool_registration.ts | 51 +++++++++++++++++++++++-------- src/lucid/tx_complete.ts | 2 +- src/lucid/tx_signed.ts | 2 +- src/utils/mod.ts | 2 ++ 4 files changed, 43 insertions(+), 14 deletions(-) diff --git a/src/examples/pool_registration.ts b/src/examples/pool_registration.ts index be734e8a..e79e66d6 100644 --- a/src/examples/pool_registration.ts +++ b/src/examples/pool_registration.ts @@ -1,8 +1,19 @@ -import { Blockfrost, C, fromHex, Lucid, PoolParams } from "../mod.ts"; +import { + Blockfrost, + C, + FreeableBucket, + Freeables, + fromHex, + Lucid, + PoolParams, +} from "../mod.ts"; + +/** When working with objects from the C module or objects that have fields we need to always free their memory when we're done to prevent memory leaks. A FreeableBucket is one way to do that.*/ +const bucket: FreeableBucket = []; const lucid = await Lucid.new( new Blockfrost("https://cardano-preview.blockfrost.io/api/v0", ""), - "Preview", + "Preview" ); lucid.selectWalletFromSeed("car rare ..."); @@ -10,18 +21,28 @@ lucid.selectWalletFromSeed("car rare ..."); /** StakePoolSigningKey_ed25519 cborHex from the cardano-cli */ const coldKey = C.PrivateKey.from_bytes( fromHex( - "58204de30f983ed860524d00059c7f2b1d63240fba805bee043604aa7ccb13d387e9", - ), + "58204de30f983ed860524d00059c7f2b1d63240fba805bee043604aa7ccb13d387e9" + ) ); +bucket.push(coldKey); /** VrfVerificationKey_PraosVRF cborHex from the cardano-cli */ -const vrfKeyHash = C.VRFVKey.from_bytes( +const vrfVKey = C.VRFVKey.from_bytes( fromHex( - "5820c9cf07d863c8a2351662c9759ca1d9858b536bab50ad575b5de161e1af18f887", - ), -).hash().to_hex(); + "5820c9cf07d863c8a2351662c9759ca1d9858b536bab50ad575b5de161e1af18f887" + ) +); +bucket.push(vrfVKey); -const poolId = coldKey.to_public().hash().to_bech32("pool"); +const vrfVKeyHash = vrfVKey.hash(); +bucket.push(vrfVKeyHash); +const vrfKeyHash = vrfVKeyHash.to_hex(); + +const publicKey = coldKey.to_public(); +bucket.push(publicKey); +const publicKeyHash = publicKey.hash(); +bucket.push(publicKeyHash); +const poolId = publicKeyHash.to_bech32("pool"); const rewardOwnerAddress = (await lucid.wallet.rewardAddress())!; @@ -37,13 +58,19 @@ const poolParams: PoolParams = { metadataUrl: "https://...", // metadata needs to be hosted already before registering the pool }; -const tx = await lucid.newTx() - .registerPool(poolParams).complete(); +const tx = lucid.newTx().registerPool(poolParams); +bucket.push(tx); +const completeTX = await lucid.newTx().registerPool(poolParams).complete(); +bucket.push(completeTX); -const signedTx = await tx.sign() +const signedTx = await completeTX + .sign() .signWithPrivateKey(coldKey.to_bech32()) .complete(); +bucket.push(signedTx); const txHash = await signedTx.submit(); +Freeables.free(...bucket); + console.log(txHash); diff --git a/src/lucid/tx_complete.ts b/src/lucid/tx_complete.ts index 981e7ea6..ce396b7e 100644 --- a/src/lucid/tx_complete.ts +++ b/src/lucid/tx_complete.ts @@ -163,7 +163,7 @@ export class TxComplete { return txHash; } - /** Since this object has WASM parameters, we must use the free method to free the parameters */ + /** Since this object has WASM fields, we must use the free method to free the fields */ free() { this.txComplete.free(); this.witnessSetBuilder.free(); diff --git a/src/lucid/tx_signed.ts b/src/lucid/tx_signed.ts index 885fa013..4d1be552 100644 --- a/src/lucid/tx_signed.ts +++ b/src/lucid/tx_signed.ts @@ -30,7 +30,7 @@ export class TxSigned { return txHash; } - /** Since this object has WASM parameters, we must use the free method to free the parameters */ + /** Since this object has WASM fields, we must use the free method to free the fields */ free() { this.txSigned.free(); } diff --git a/src/utils/mod.ts b/src/utils/mod.ts index 429b6c2c..972e4c7b 100644 --- a/src/utils/mod.ts +++ b/src/utils/mod.ts @@ -1,3 +1,5 @@ export * from "./cost_model.ts"; export * from "./utils.ts"; export * from "./merkle_tree.ts"; +export * from "./cml.ts"; +export * from "./freeable.ts"; From 62e6b2d762fd7686380078ac5ea63f2321e9a13c Mon Sep 17 00:00:00 2001 From: yHSJ Date: Sat, 4 Nov 2023 17:43:28 -0400 Subject: [PATCH 53/54] chore: prepare for npm release --- blueprint.ts | 137 ++++++++++++++++++++++++++++----------------------- package.json | 10 ++-- 2 files changed, 80 insertions(+), 67 deletions(-) diff --git a/blueprint.ts b/blueprint.ts index cf2966ab..75bb56f2 100644 --- a/blueprint.ts +++ b/blueprint.ts @@ -36,28 +36,31 @@ type Blueprint = { compiledCode: string; hash: string; }[]; - definitions: Record; + definitions: Record< + string, + { + title: string; + schema: { + $ref: string; + }; + } + >; }; const plutusJson: Blueprint = JSON.parse( - await Deno.readTextFile("plutus.json"), + await Deno.readTextFile("plutus.json") ); -const plutusVersion = "Plutus" + - plutusJson.preamble.plutusVersion.toUpperCase(); +const plutusVersion = + "Plutus" + plutusJson.preamble.plutusVersion.toUpperCase(); const definitions = plutusJson.definitions; const imports = `// deno-lint-ignore-file import { applyParamsToScript, Data, Validator } from "${ flags.npm - ? "lucid-cardano" - : `https://deno.land/x/lucid@${packageJson.version}/mod.ts` + ? "jucid-cardano" + : `https://deno.land/x/jucid@${packageJson.version}/mod.ts` }"`; const validators = plutusJson.validators.map((validator) => { @@ -79,10 +82,10 @@ const validators = plutusJson.validators.map((validator) => { dataType: "list", items: params.map((param) => resolveSchema(param.schema, definitions)), }; - const paramsArgs = params.map(( - param, - index, - ) => [snakeToCamel(param.title), schemaToType(paramsSchema.items[index])]); + const paramsArgs = params.map((param, index) => [ + snakeToCamel(param.title), + schemaToType(paramsSchema.items[index]), + ]); const script = validator.compiledCode; @@ -96,9 +99,9 @@ const validators = plutusJson.validators.map((validator) => { export const ${name} = Object.assign( function (${paramsArgs.map((param) => param.join(":")).join(",")}) {${ paramsArgs.length > 0 - ? `return { type: "${plutusVersion}", script: applyParamsToScript("${script}", [${ - paramsArgs.map((param) => param[0]).join(",") - }], ${JSON.stringify(paramsSchema)} as any) };` + ? `return { type: "${plutusVersion}", script: applyParamsToScript("${script}", [${paramsArgs + .map((param) => param[0]) + .join(",")}], ${JSON.stringify(paramsSchema)} as any) };` : `return {type: "${plutusVersion}", script: "${script}"};` }}, ${datum ? `{${datumTitle}: ${JSON.stringify(datumSchema)}},` : ""} @@ -116,7 +119,7 @@ await new Deno.Command(Deno.execPath(), { console.log( "%cGenerated %cplutus.ts", "color: green; font-weight: bold", - "font-weight: bold", + "font-weight: bold" ); function resolveSchema(schema: any, definitions: any): any { @@ -153,8 +156,9 @@ function resolveSchema(schema: any, definitions: any): any { }; } else { if (schema["$ref"]) { - const refKey = - schema["$ref"].replaceAll("~1", "/").split("#/definitions/")[1]; + const refKey = schema["$ref"] + .replaceAll("~1", "/") + .split("#/definitions/")[1]; return resolveSchema(definitions[refKey], definitions); } else { return schema; @@ -177,11 +181,11 @@ function schemaToType(schema: any): string { if (isVoid(schema)) { return "undefined"; } else { - return `{${ - schema.fields.map((field: any) => - `${field.title || "wrapper"}:${schemaToType(field)}` - ).join(";") - }}`; + return `{${schema.fields + .map( + (field: any) => `${field.title || "wrapper"}:${schemaToType(field)}` + ) + .join(";")}}`; } } case "enum": { @@ -195,35 +199,37 @@ function schemaToType(schema: any): string { if (isNullable(schema)) { return `${schemaToType(schema.anyOf[0].fields[0])} | null`; } - return schema.anyOf.map((entry: any) => - entry.fields.length === 0 - ? `"${entry.title}"` - : `{${entry.title}: ${ - entry.fields[0].title - ? `{${ - entry.fields.map((field: any) => - [field.title, schemaToType(field)].join(":") - ).join(",") - }}}` - : `[${ - entry.fields.map((field: any) => schemaToType(field)).join(",") - }]}` - }` - ).join(" | "); + return schema.anyOf + .map((entry: any) => + entry.fields.length === 0 + ? `"${entry.title}"` + : `{${entry.title}: ${ + entry.fields[0].title + ? `{${entry.fields + .map((field: any) => + [field.title, schemaToType(field)].join(":") + ) + .join(",")}}}` + : `[${entry.fields + .map((field: any) => schemaToType(field)) + .join(",")}]}` + }` + ) + .join(" | "); } case "list": { if (schema.items instanceof Array) { - return `[${ - schema.items.map((item: any) => schemaToType(item)).join(",") - }]`; + return `[${schema.items + .map((item: any) => schemaToType(item)) + .join(",")}]`; } else { return `Array<${schemaToType(schema.items)}>`; } } case "map": { - return `Map<${schemaToType(schema.keys)}, ${ - schemaToType(schema.values) - }>`; + return `Map<${schemaToType(schema.keys)}, ${schemaToType( + schema.values + )}>`; } case undefined: { return "Data"; @@ -233,8 +239,11 @@ function schemaToType(schema: any): string { } function isBoolean(shape: any): boolean { - return shape.anyOf && shape.anyOf[0]?.title === "False" && - shape.anyOf[1]?.title === "True"; + return ( + shape.anyOf && + shape.anyOf[0]?.title === "False" && + shape.anyOf[1]?.title === "True" + ); } function isVoid(shape: any): boolean { @@ -242,26 +251,30 @@ function isVoid(shape: any): boolean { } function isNullable(shape: any): boolean { - return shape.anyOf && shape.anyOf[0]?.title === "Some" && - shape.anyOf[1]?.title === "None"; + return ( + shape.anyOf && + shape.anyOf[0]?.title === "Some" && + shape.anyOf[1]?.title === "None" + ); } function snakeToCamel(s: string): string { const withUnderscore = s.charAt(0) === "_" ? s.charAt(0) : ""; - return withUnderscore + - (withUnderscore ? s.slice(1) : s).toLowerCase().replace( - /([-_][a-z])/g, - (group) => - group - .toUpperCase() - .replace("-", "") - .replace("_", ""), - ); + return ( + withUnderscore + + (withUnderscore ? s.slice(1) : s) + .toLowerCase() + .replace(/([-_][a-z])/g, (group) => + group.toUpperCase().replace("-", "").replace("_", "") + ) + ); } function upperFirst(s: string): string { const withUnderscore = s.charAt(0) === "_" ? s.charAt(0) : ""; - return withUnderscore + + return ( + withUnderscore + s.charAt(withUnderscore ? 1 : 0).toUpperCase() + - s.slice((withUnderscore ? 1 : 0) + 1); + s.slice((withUnderscore ? 1 : 0) + 1) + ); } diff --git a/package.json b/package.json index 925a9938..61348eba 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { - "name": "lucid-cardano", - "version": "0.10.7", + "name": "jucid-cardano", + "version": "1.0.0-alpha.1", "license": "MIT", - "author": "Alessandro Konrad", - "description": "Lucid is a library, which allows you to create Cardano transactions and off-chain code for your Plutus contracts in JavaScript, Deno and Node.js.", - "repository": "https://github.com/spacebudz/lucid" + "author": "Josh Marchand", + "description": "Jucid is a fork of Lucid which allows you to create Cardano transactions and off-chain code for your Plutus contracts in JavaScript, Deno and Node.js without introducing memory leaks.", + "repository": "https://github.com/yHSJ/jucid" } From 8c76fcdfef0a4cdb9bb19262927afbd972b0f7fb Mon Sep 17 00:00:00 2001 From: yHSJ Date: Sat, 4 Nov 2023 17:52:52 -0400 Subject: [PATCH 54/54] chore: update readme --- README.md | 69 ++++++++++++++++++++++--------------------------------- 1 file changed, 28 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 367266da..95955abe 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,34 @@

- -

Lucid

-

Lucid is a library, which allows you to create Cardano transactions and off-chain code for your Plutus contracts in JavaScript, Deno and Node.js.

+

Jucid

+

Jucid a fork of Lucid, which allows you to create Cardano transactions and off-chain code for your Plutus contracts in JavaScript, Deno and Node.js without worrying about memory leaks.

- - - + + + - - - - + + - - - + + +

+### Lucid Fork + +Jucid is JSHy's fork of Lucid (Jshy + Lucid = Jucid). Lucid accidentally introduces memory leaks into applications due to incorrect memory management of WASM objects. Jucid handles internal memory correctly, exposes new interfaces to allow you to management objects you use correclty, and is a drop-in replacement for Lucid. Once this fork is merged into Lucid, it is unlikely it will be maintained. + ### Get started #### NPM ``` -npm install lucid-cardano +npm install jucid-cardano ``` #### Deno 🦕 @@ -35,19 +36,19 @@ npm install lucid-cardano For JavaScript and TypeScript ```js -import { Lucid } from "https://deno.land/x/lucid@0.10.7/mod.ts"; +import { jucid } from "https://deno.land/x/jucid@1.0.0-alpha.1/mod.ts"; ``` #### Web ```html ``` -### +### ### Build from source @@ -61,31 +62,29 @@ Outputs a `dist` folder ### Examples -- [Basic examples](./src/examples/) -- [Next.js Blockfrost Proxy API Example](https://github.com/GGAlanSmithee/cardano-lucid-blockfrost-proxy-example) +- Coming Soon ### Basic usage ```js -// import { Blockfrost, Lucid } from "https://deno.land/x/lucid@0.10.7/mod.ts"; Deno -import { Blockfrost, Lucid } from "lucid-cardano"; // NPM +// import { Blockfrost, Lucid } from "https://deno.land/x/jucid@1.0.0-alpha.1/mod.ts"; Deno +import { Blockfrost, Lucid } from "jucid-cardano"; // NPM const lucid = await Lucid.new( new Blockfrost("https://cardano-preview.blockfrost.io/api/v0", ""), - "Preview", + "Preview" ); // Assumes you are in a browser environment const api = await window.cardano.nami.enable(); lucid.selectWallet(api); -const tx = await lucid.newTx() - .payToAddress("addr...", { lovelace: 5000000n }) - .complete(); - -const signedTx = await tx.sign().complete(); +const tx = lucid.newTx().payToAddress("addr...", { lovelace: 5000000n }); +const completeTx = await tx.complete(); +const signedTx = await completeTx.sign(); const txHash = await signedTx.submit(); +Freeables.free(tx, completeTx, signedTx); console.log(txHash); ``` @@ -114,7 +113,7 @@ deno task test:core ### Docs -[View docs](https://doc.deno.land/https://deno.land/x/lucid/mod.ts) 📖 +[View docs](https://doc.deno.land/https://deno.land/x/jucid/mod.ts) 📖 You can generate documentation with: @@ -143,15 +142,3 @@ project's `package.json`. Otherwise you will get import issues. Contributions and PRs are welcome!\ The [contribution instructions](./CONTRIBUTING.md). - -Join us on [Discord](https://discord.gg/82MWs63Tdm)! - -### Use Lucid with React - -[use-cardano](https://use-cardano.alangaming.com/) a React context, hook and set -of components built on top of Lucid. - -### Use Lucid with Next.js - -[Cardano Starter Kit](https://cardano-starter-kit.alangaming.com/) a Next.js -starter kit for building Cardano dApps.