Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/loose-ears-cheat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fleet-sdk/core": minor
---

Add ErgoTree named constants replacing support
5 changes: 5 additions & 0 deletions .changeset/sour-ideas-flow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fleet-sdk/core": minor
---

Add ErgoTree construction from ergoc compiler JSON output
98 changes: 96 additions & 2 deletions packages/core/src/models/ergoTree.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Network } from "@fleet-sdk/common";
import { hex } from "@fleet-sdk/crypto";
import { SBool, SigmaByteReader, estimateVLQSize } from "@fleet-sdk/serializer";
import { SBool, SLong, SigmaByteReader, estimateVLQSize } from "@fleet-sdk/serializer";
import { ErgoTree$ } from "sigmastate-js/main";
import { describe, expect, it, test } from "vitest";
import { SInt } from "../constantSerializer";
import { ErgoAddress } from "./ergoAddress";
import { ErgoTree } from "./ergoTree";
import { ErgoTree, type JsonCompilerOutput } from "./ergoTree";

describe("ErgoTree model", () => {
test.each([
Expand Down Expand Up @@ -290,3 +290,97 @@ describe("Encoding", () => {
);
});
});

describe("JSON object construction", () => {
const testVectors: {
name: string;
tree: string;
compilerOutput: JsonCompilerOutput;
}[] = [
{
name: "ErgoTree with segregated constants",
tree: "1a740a04c80104900304d8040e20fbbaac7337d051c10fc3da0ccb864f4d32d40027551e1c3ea3ce361f39b91e400e106b75d7c82b1c99619f2c6ea6d6585cc904ca01040201000490030404d801d601830304730073017302d1ededed9373037304917305b272017306007307917308b27201730900",
compilerOutput: {
header: "1a",
expressionTree:
"d801d601830304730073017302d1ededed9373037304917305b272017306007307917308b27201730900",
constants: [
{ value: "04c801", type: "Int" },
{ value: "049003", type: "Int", name: "_deadline_two" },
{ value: "04d804", type: "Int" },
{
value: "0e20fbbaac7337d051c10fc3da0ccb864f4d32d40027551e1c3ea3ce361f39b91e40",
type: "Coll[Byte]"
},
{
value: "0e106b75d7c82b1c99619f2c6ea6d6585cc9",
type: "Coll[Byte]",
name: "$tokenId",
description: "payment token id"
},
{ value: "04ca01", type: "Int", name: "_deadline", description: "Payment deadline" },
{ value: "0402", type: "Int" },
{ value: "0100", type: "Bool" },
{ value: "049003", type: "Int", name: "_deadline_two" },
{ value: "0404", type: "Int" }
]
}
},
{
name: "Without size flag",
tree: "10020580897a0402d19173007e730105",
compilerOutput: {
header: "10",
expressionTree: "d19173007e730105",
constants: [
{ value: "0580897a", type: "Long" },
{ value: "0402", type: "Int" }
]
}
},
{
name: "No constants segregation",
tree: "0a08d1910580897a0402",
compilerOutput: {
header: "0a",
expressionTree: "d1910580897a0402"
}
}
];

test.each(testVectors)("Should construct from JSON object ($name)", (tv) => {
const tree = ErgoTree.from(tv.compilerOutput);
expect(tree.toHex()).to.be.equal(tv.tree);
});

it("Should support named constants", () => {
const tv = {
tree: "1a17030580897a0400059003d191b283010573007301007302",
compilerOutput: {
header: "1a",
expressionTree: "d191b283010573007301007302",
constants: [
{ value: "0580897a", type: "Long" },
{ value: "0400", type: "Int" },
{ value: "059003", type: "Long", name: "price2" }
]
}
};

const tree = ErgoTree.from(tv.compilerOutput);
expect(tree.toHex()).to.be.equal(tv.tree); // no changes made

// should replace named constant
tree.replaceConstant("price2", SLong(2n));
expect(tree.constants[2].data).to.be.equal(2n);
expect(tree.toHex()).not.to.be.equal(tv.tree);

// replacing by number should work too
expect(() => tree.replaceConstant(0, SLong(3n))).not.to.throw();

// should throw if named constant is not found
expect(() => tree.replaceConstant("non-existing", SLong(2n))).to.throw(
"Constant with name 'non-existing' not found."
);
});
});
95 changes: 87 additions & 8 deletions packages/core/src/models/ergoTree.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { type Base58String, type HexString, Network, ergoTreeHeaderFlags } from "@fleet-sdk/common";
import { hex } from "@fleet-sdk/crypto";
import {
type Base58String,
type HexString,
Network,
byteSizeOf,
ergoTreeHeaderFlags
} from "@fleet-sdk/common";
import { type ByteInput, hex } from "@fleet-sdk/crypto";
import {
SConstant,
SigmaByteReader,
Expand All @@ -18,14 +24,29 @@ export class ErgoTree {
#byteReader?: SigmaByteReader;
#root!: Uint8Array;
#constants: SConstant[] = [];
#constantNames: Map<string, number> = new Map();

constructor(input: HexString | Uint8Array, network?: Network) {
constructor(input: ByteInput, network?: Network) {
this.#byteReader = new SigmaByteReader(input);

this.#header = this.#byteReader.readByte();
this.#network = network ?? Network.Mainnet;
}

static from(input: JsonCompilerOutput, network?: Network): ErgoTree {
const tree = new ErgoTree(reconstructTreeFromObject(input).toBytes(), network);
if (tree.hasSegregatedConstants && input.constants?.length) {
for (let i = 0; i < input.constants.length; i++) {
const constant = input.constants[i];
if (!constant.name) continue;

tree.#nameConstant(i, constant.name);
}
}

return tree;
}

get bytes(): Uint8Array {
return this.serialize();
}
Expand All @@ -35,15 +56,15 @@ export class ErgoTree {
}

get version(): number {
return this.#header & VERSION_MASK;
return getVersion(this.#header);
}

get hasSegregatedConstants(): boolean {
return (this.#header & ergoTreeHeaderFlags.constantSegregation) !== 0;
return hasFlag(this.#header, ergoTreeHeaderFlags.constantSegregation);
}

get hasSize(): boolean {
return (this.#header & ergoTreeHeaderFlags.sizeInclusion) !== 0;
return hasFlag(this.#header, ergoTreeHeaderFlags.sizeInclusion);
}

get constants(): ReadonlyArray<SConstant> {
Expand All @@ -59,12 +80,19 @@ export class ErgoTree {
return !!this.#root;
}

replaceConstant(index: number, constant: SConstant): ErgoTree {
replaceConstant(index: number | string, constant: SConstant): ErgoTree {
if (!this.hasSegregatedConstants) throw new Error("Constant segregation is not enabled.");

this.#parse();

const oldConst = this.#constants?.[index];
if (typeof index === "string") {
const namedIndex = this.#constantNames.get(index);
if (namedIndex === undefined) throw new Error(`Constant with name '${index}' not found.`);

index = namedIndex;
}

const oldConst = this.#constants[index];
if (!oldConst) throw new Error(`Constant at index ${index} not found.`);
if (oldConst.type.toString() !== constant.type.toString()) {
throw new Error(
Expand All @@ -77,6 +105,11 @@ export class ErgoTree {
return this;
}

#nameConstant(index: number, name: string): ErgoTree {
this.#constantNames.set(name, index);
return this;
}

toHex(): HexString {
return hex.encode(this.serialize());
}
Expand Down Expand Up @@ -129,3 +162,49 @@ export class ErgoTree {
return this;
}
}

function hasFlag(header: number, flag: number): boolean {
return (header & flag) !== 0;
}

function getVersion(header: number): number {
return header & VERSION_MASK;
}

function reconstructTreeFromObject(input: JsonCompilerOutput): SigmaByteWriter {
const numHead = Number.parseInt(input.header, 16);
const sizeDelimited = hasFlag(numHead, ergoTreeHeaderFlags.sizeInclusion);
const constSegregated = hasFlag(numHead, ergoTreeHeaderFlags.constantSegregation);

let len = input.constants?.reduce((acc, c) => acc + byteSizeOf(c.value), 0) ?? 0;
len += estimateVLQSize(len);
const constBytes =
constSegregated && input.constants
? new SigmaByteWriter(len)
.writeArray(input.constants, (w, c) => w.writeHex(c.value))
.toBytes()
: new Uint8Array(0);

len = constBytes.length + byteSizeOf(input.header) + byteSizeOf(input.expressionTree);
len += estimateVLQSize(len);
const writer = new SigmaByteWriter(len).write(numHead);

if (sizeDelimited) {
writer.writeUInt(constBytes.length + byteSizeOf(input.expressionTree));
}

return writer.writeBytes(constBytes).writeHex(input.expressionTree);
}

export interface ConstantInfo {
value: string;
type: string;
name?: string;
description?: string;
}

export interface JsonCompilerOutput {
header: string;
expressionTree: string;
constants?: ConstantInfo[];
}