From 9d0d87e3ae7743925ad3979a2b6f1cbe6dd107c1 Mon Sep 17 00:00:00 2001 From: yHSJ Date: Wed, 1 Nov 2023 11:37:22 -0400 Subject: [PATCH 01/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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 5164858bff772be2ffcb18a73cdc81d596ffb32c Mon Sep 17 00:00:00 2001 From: yHSJ Date: Sat, 4 Nov 2023 18:16:00 -0400 Subject: [PATCH 51/51] feat: manage memory in kupmios.kupmiosUtxosToUtxos --- src/provider/kupmios.ts | 268 ++++++++++++++++++++++------------------ 1 file changed, 145 insertions(+), 123 deletions(-) diff --git a/src/provider/kupmios.ts b/src/provider/kupmios.ts index 06214db7..919a530d 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,95 @@ 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 - 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; - })); + (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") { + 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; } }