From 9d0d87e3ae7743925ad3979a2b6f1cbe6dd107c1 Mon Sep 17 00:00:00 2001 From: yHSJ Date: Wed, 1 Nov 2023 11:37:22 -0400 Subject: [PATCH 01/14] 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/14] 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 25ad1f9c1244d6d6d2af4512ecadfb9c401cb33a Mon Sep 17 00:00:00 2001 From: Joaquin Hoyos Date: Wed, 1 Nov 2023 23:00:17 -0300 Subject: [PATCH 03/14] fix memory leaks from Tx --- src/lucid/tx.ts | 674 ++++++++++++++++-------------------------- src/utils/cml.ts | 245 +++++++++++++++ src/utils/freeable.ts | 13 + 3 files changed, 513 insertions(+), 419 deletions(-) create mode 100644 src/utils/cml.ts create mode 100644 src/utils/freeable.ts diff --git a/src/lucid/tx.ts b/src/lucid/tx.ts index 3c1f06b2..f1e3b01c 100644 --- a/src/lucid/tx.ts +++ b/src/lucid/tx.ts @@ -21,6 +21,15 @@ 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, @@ -48,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; @@ -68,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; @@ -98,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; } @@ -133,10 +149,11 @@ 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); + Freeables.free(output, addr, value); that.txBuilder.add_output(output); }); return this; @@ -149,42 +166,61 @@ export class Tx { 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; } @@ -216,41 +252,21 @@ 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" - ? 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; } @@ -260,30 +276,19 @@ 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" - ? 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; } @@ -293,38 +298,21 @@ 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" - ? 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; } @@ -337,11 +325,9 @@ 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); + Freeables.free(certificate, poolRegistration); }); return this; } @@ -357,9 +343,8 @@ 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); + Freeables.free(poolRegistration, certificate); that.txBuilder.add_certificate(certificate, undefined); }); @@ -371,10 +356,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; } @@ -385,21 +371,12 @@ export class Tx { 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; } @@ -412,15 +389,14 @@ 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."); } - 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."); @@ -431,9 +407,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; } @@ -441,9 +417,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; } @@ -451,17 +427,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; } @@ -469,11 +446,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, ); + Freeables.free(labelNum); }); return this; } @@ -481,9 +460,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; } @@ -522,94 +503,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, - ), - ); + ); + + return new TxComplete(this.lucid, tx); + } finally { + Freeables.free(...bucket); + } } /** Return the current transaction body in Hex encoded Cbor. */ @@ -623,132 +588,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..0121dddd --- /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 {}; + } +} diff --git a/src/utils/freeable.ts b/src/utils/freeable.ts new file mode 100644 index 00000000..a3ace4d8 --- /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 | null)[]) { + bucket.forEach((freeable) => { + freeable?.free(); + }); + } +} From d9cc706fc7a1dc3f8b11126f4f143f23bfa008c9 Mon Sep 17 00:00:00 2001 From: yHSJ Date: Thu, 2 Nov 2023 12:07:00 -0400 Subject: [PATCH 04/14] 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 05/14] 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 093c0914d32cc5604a7773ec0cf5baa491cc9216 Mon Sep 17 00:00:00 2001 From: yHSJ Date: Wed, 1 Nov 2023 13:51:12 -0400 Subject: [PATCH 06/14] 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 a93935b970885af2888053c4b6e5d19d22339209 Mon Sep 17 00:00:00 2001 From: yHSJ Date: Wed, 1 Nov 2023 13:52:48 -0400 Subject: [PATCH 07/14] 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 78568f65aeedbcbb25a5abb08bfbffe8357904cf Mon Sep 17 00:00:00 2001 From: yHSJ Date: Wed, 1 Nov 2023 14:06:54 -0400 Subject: [PATCH 08/14] 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 6e0fca87abb6a79e9d3d3aee0abf463054f9537a Mon Sep 17 00:00:00 2001 From: yHSJ Date: Wed, 1 Nov 2023 14:13:24 -0400 Subject: [PATCH 09/14] 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 b8957f2ea7f5b28ae63311935f779c945968b009 Mon Sep 17 00:00:00 2001 From: yHSJ Date: Wed, 1 Nov 2023 14:36:11 -0400 Subject: [PATCH 10/14] 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 b5a66383db537c13f9c6ffa450ad4c72acd17050 Mon Sep 17 00:00:00 2001 From: yHSJ Date: Wed, 1 Nov 2023 14:46:29 -0400 Subject: [PATCH 11/14] 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 d560c27b641fcb47faae0fedc07448838a8e917a Mon Sep 17 00:00:00 2001 From: yHSJ Date: Wed, 1 Nov 2023 14:47:08 -0400 Subject: [PATCH 12/14] 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 e5b4233b0b743f22566fad4be131e94852cc7e72 Mon Sep 17 00:00:00 2001 From: yHSJ Date: Wed, 1 Nov 2023 14:58:02 -0400 Subject: [PATCH 13/14] 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 b3f323cfef8e3953d570db9e8e2e2bd4555a46e2 Mon Sep 17 00:00:00 2001 From: Joaquin Hoyos Date: Wed, 1 Nov 2023 18:30:10 -0300 Subject: [PATCH 14/14] 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); });