Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add managed decimal support (as in the rust framework) #477

Merged
merged 21 commits into from
Sep 10, 2024
Merged
Show file tree
Hide file tree
Changes from 11 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
17 changes: 10 additions & 7 deletions src/abi/typeFormula.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
export class TypeFormula {
name: string;
metadata: any;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe change type to string here, as well

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here we will let any because is the general object

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't you think it would be better to use object type instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it can be also a primitive that's why I think any is a better option here

typeParameters: TypeFormula[];

constructor(name: string, typeParameters: TypeFormula[]) {
constructor(name: string, typeParameters: TypeFormula[], metadata?: any) {
this.name = name;
this.typeParameters = typeParameters;
this.metadata = metadata;
}

toString(): string {
if (this.typeParameters.length > 0) {
const typeParameters = this.typeParameters.map((typeParameter) => typeParameter.toString()).join(", ");
return `${this.name}<${typeParameters}>`;
} else {
return this.name;
}
const hasTypeParameters = this.typeParameters.length > 0;
const typeParameters = hasTypeParameters
? `<${this.typeParameters.map((tp) => tp.toString()).join(", ")}>`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

< and > can also stay outside the typeParameters string, and be placed in baseName (below). Just an opinion, optional.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if move down when there will be no typeParameters we will have <> with empty string

: "";
const baseName = `${this.name}${typeParameters}`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did you intentionally left out the <>? It used to be: return ${this.name}<${typeParameters}>;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<> is already added when we build the variable typeParameters <${this.typeParameters.map((tp) => tp.toString()).join(", ")}>


return this.metadata !== undefined ? `${baseName}*${this.metadata}*` : baseName;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All good to append the metadata after an asterisk 👍

}
}
8 changes: 5 additions & 3 deletions src/abi/typeFormulaParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export class TypeFormulaParser {

parseExpression(expression: string): TypeFormula {
expression = expression.trim();

const tokens = this.tokenizeExpression(expression).filter((token) => token !== TypeFormulaParser.COMMA);
const stack: any[] = [];

Expand All @@ -32,7 +31,6 @@ export class TypeFormulaParser {
stack.push(token);
}
}

if (stack.length !== 1) {
throw new Error(`Unexpected stack length at end of parsing: ${stack.length}`);
}
Expand Down Expand Up @@ -83,6 +81,11 @@ export class TypeFormulaParser {
private acquireTypeWithParameters(stack: any[]): TypeFormula {
const typeParameters = this.acquireTypeParameters(stack);
const typeName = stack.pop();

if (typeName === "ManagedDecimal" || typeName === "ManagedDecimalSigned") {
const typeFormula = new TypeFormula(typeName, [], typeParameters[0].name);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can define a separate variable above, such as const metadata = typeParameters[0].name - more obvious that we're treating the name of the first "type parameter" not as a type parameter, but as a special metadata (new concept).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done 👍

return typeFormula;
}
const typeFormula = new TypeFormula(typeName, typeParameters.reverse());
return typeFormula;
}
Expand All @@ -92,7 +95,6 @@ export class TypeFormulaParser {

while (true) {
const item = stack.pop();

if (item === undefined) {
throw new Error("Badly specified type parameters");
}
Expand Down
9 changes: 9 additions & 0 deletions src/smartcontracts/codec/binary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
Tuple,
ArrayVecType,
ArrayVec,
ManagedDecimalType,
ManagedDecimalValue,
} from "../typesystem";
import { guardTrue } from "../../utils";
import { OptionValueBinaryCodec } from "./option";
Expand All @@ -25,6 +27,7 @@ import { StructBinaryCodec } from "./struct";
import { EnumBinaryCodec } from "./enum";
import { TupleBinaryCodec } from "./tuple";
import { ArrayVecBinaryCodec } from "./arrayVec";
import { ManagedDecimalCodec } from "./managedDecimal";

export class BinaryCodec {
readonly constraints: BinaryCodecConstraints;
Expand All @@ -35,6 +38,7 @@ export class BinaryCodec {
private readonly structCodec: StructBinaryCodec;
private readonly tupleCodec: TupleBinaryCodec;
private readonly enumCodec: EnumBinaryCodec;
private readonly managedDecimalCodec: ManagedDecimalCodec;

constructor(constraints: BinaryCodecConstraints | null = null) {
this.constraints = constraints || new BinaryCodecConstraints();
Expand All @@ -45,6 +49,7 @@ export class BinaryCodec {
this.structCodec = new StructBinaryCodec(this);
this.tupleCodec = new TupleBinaryCodec(this);
this.enumCodec = new EnumBinaryCodec(this);
this.managedDecimalCodec = new ManagedDecimalCodec(this);
}

decodeTopLevel<TResult extends TypedValue = TypedValue>(buffer: Buffer, type: Type): TResult {
Expand All @@ -58,6 +63,7 @@ export class BinaryCodec {
onStruct: () => this.structCodec.decodeTopLevel(buffer, <StructType>type),
onTuple: () => this.tupleCodec.decodeTopLevel(buffer, <TupleType>type),
onEnum: () => this.enumCodec.decodeTopLevel(buffer, <EnumType>type),
onManagedDecimal: () => this.managedDecimalCodec.decodeTopLevel(buffer, <ManagedDecimalType>type),
});

return <TResult>typedValue;
Expand All @@ -74,6 +80,7 @@ export class BinaryCodec {
onStruct: () => this.structCodec.decodeNested(buffer, <StructType>type),
onTuple: () => this.tupleCodec.decodeNested(buffer, <TupleType>type),
onEnum: () => this.enumCodec.decodeNested(buffer, <EnumType>type),
onManagedDecimal: () => this.managedDecimalCodec.decodeNested(buffer, <ManagedDecimalType>type),
});

return [<TResult>typedResult, decodedLength];
Expand All @@ -90,6 +97,7 @@ export class BinaryCodec {
onStruct: () => this.structCodec.encodeNested(<Struct>typedValue),
onTuple: () => this.tupleCodec.encodeNested(<Tuple>typedValue),
onEnum: () => this.enumCodec.encodeNested(<EnumValue>typedValue),
onManagedDecimal: () => this.managedDecimalCodec.encodeNested(<ManagedDecimalValue>typedValue),
});
}

Expand All @@ -104,6 +112,7 @@ export class BinaryCodec {
onStruct: () => this.structCodec.encodeTopLevel(<Struct>typedValue),
onTuple: () => this.tupleCodec.encodeTopLevel(<Tuple>typedValue),
onEnum: () => this.enumCodec.encodeTopLevel(<EnumValue>typedValue),
onManagedDecimal: () => this.managedDecimalCodec.encodeTopLevel(<ManagedDecimalValue>typedValue),
});
}
}
Expand Down
58 changes: 58 additions & 0 deletions src/smartcontracts/codec/managedDecimal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import BigNumber from "bignumber.js";
import { BigUIntValue, ManagedDecimalType, ManagedDecimalValue, U32Value } from "../typesystem";
import { BinaryCodec } from "./binary";
import { bufferToBigInt } from "./utils";

export class ManagedDecimalCodec {
private readonly binaryCodec: BinaryCodec;

constructor(binaryCodec: BinaryCodec) {
this.binaryCodec = binaryCodec;
}

decodeNested(buffer: Buffer, type: ManagedDecimalType): [ManagedDecimalValue, number] {
const length = buffer.readUInt32BE(0);
const payload = buffer.slice(0, length);

const result = this.decodeTopLevel(payload, type);
const decodedLength = length;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

length can simply be used instead of creating a new variable, but not a big deal.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


return [result, decodedLength];
}

decodeTopLevel(buffer: Buffer, type: ManagedDecimalType): ManagedDecimalValue {
if (buffer.length === 0) {
return new ManagedDecimalValue(new BigNumber(0), 2);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Magic number 2 can be a constant - is it default scale of managed decimal?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

}

const isUsize = type.getMetadata() === "usize";

if (isUsize) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it worth to add maybe a function on ManagedDecimalType, such as type.isVariable()?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

const u32Size = 4;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

const bigUintSize = buffer.length - u32Size;

const bigUint = new BigNumber(buffer.slice(0, bigUintSize).toString("hex"), 16);
const u32 = buffer.readUInt32BE(bigUintSize);

return new ManagedDecimalValue(bigUint, u32);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variables should be named value and scale.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

}

const value = bufferToBigInt(buffer);
return new ManagedDecimalValue(value, parseInt(type.getMetadata()));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can have a variable const scale = parseInt....

By the way, no issue with parseInt("usize")?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there will be no issue because here we know is not a variable decimal

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

}

encodeNested(value: ManagedDecimalValue): Buffer {
let buffers: Buffer[] = [];
if (value.getType().getMetadata() == "usize") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it's worth to add a function on ManagedDecimalValue, such as isVariable()?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can also say (optional):

const valueBuffer = uffer.from(this.binaryCodec.encodeTopLevel(new BigUIntValue(value.valueOf());

if (!value.isVariable) {
    return valueBuffer;
}

const scaleBuffer = Buffer.from(this.binaryCodec.encodeNested(new U32Value(value.getScale()));
return Buffer.concat([...]);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not working becauze if is not variable the value is encodet top level if is variable the value is encodedNested

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done, I add it on type and use it here

buffers.push(Buffer.from(this.binaryCodec.encodeNested(new BigUIntValue(value.valueOf()))));
buffers.push(Buffer.from(this.binaryCodec.encodeNested(new U32Value(value.getScale()))));
} else {
buffers.push(Buffer.from(this.binaryCodec.encodeTopLevel(new BigUIntValue(value.valueOf()))));
}
return Buffer.concat(buffers);
}

encodeTopLevel(value: ManagedDecimalValue): Buffer {
return this.encodeNested(value);
}
}
113 changes: 113 additions & 0 deletions src/smartcontracts/interaction.local.net.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { ResultsParser } from "./resultsParser";
import { TransactionWatcher } from "../transactionWatcher";
import { SmartContractQueriesController } from "../smartContractQueriesController";
import { QueryRunnerAdapter } from "../adapters/queryRunnerAdapter";
import { ManagedDecimalSignedValue, ManagedDecimalValue } from "./typesystem";

describe("test smart contract interactor", function () {
let provider = createLocalnetProvider();
Expand Down Expand Up @@ -184,6 +185,118 @@ describe("test smart contract interactor", function () {
assert.isTrue(typedBundle.returnCode.equals(ReturnCode.Ok));
});

it("should interact with 'basic-features' (local testnet) using the SmartContractTransactionsFactory", async function () {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not using SmartContractTransactionsFactory.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test is very nice. For education purposes, maybe it's better to have it using the new transactions factory.

this.timeout(140000);

let abiRegistry = await loadAbiRegistry("src/testdata/basic-features.abi.json");
let contract = new SmartContract({ abi: abiRegistry });
let controller = new ContractController(provider);

let network = await provider.getNetworkConfig();
await alice.sync(provider);

// Deploy the contract
let deployTransaction = await prepareDeployment({
contract: contract,
deployer: alice,
codePath: "src/testdata/basic-features.wasm",
gasLimit: 600000000,
initArguments: [],
chainID: network.ChainID,
});

let {
bundle: { returnCode },
} = await controller.deploy(deployTransaction);
assert.isTrue(returnCode.isSuccess());

let returnEgldInteraction = <Interaction>(
contract.methods
.returns_egld_decimal([])
.withGasLimit(10000000)
.withChainID(network.ChainID)
.withSender(alice.address)
.withValue(1)
);

// returnEgld()
let returnEgldTransaction = returnEgldInteraction
.withSender(alice.address)
.useThenIncrementNonceOf(alice.account)
.buildTransaction();

let additionInteraction = <Interaction>contract.methods
.managed_decimal_addition([new ManagedDecimalValue(2, 2), new ManagedDecimalValue(3, 2)])
.withGasLimit(10000000)
.withChainID(network.ChainID)
.withSender(alice.address)
.withValue(0);

// addition()
let additionTransaction = additionInteraction
.withSender(alice.address)
.useThenIncrementNonceOf(alice.account)
.buildTransaction();

// log
let mdLnInteraction = <Interaction>contract.methods
.managed_decimal_ln([new ManagedDecimalValue(23, 9)])
.withGasLimit(10000000)
.withChainID(network.ChainID)
.withSender(alice.address)
.withValue(0);

// mdLn()
let mdLnTransaction = mdLnInteraction
.withSender(alice.address)
.useThenIncrementNonceOf(alice.account)
.buildTransaction();

let additionVarInteraction = <Interaction>contract.methods
.managed_decimal_addition_var([
new ManagedDecimalValue(378298000000, 9, true),
new ManagedDecimalValue(378298000000, 9, true),
])
.withGasLimit(50000000)
.withChainID(network.ChainID)
.withSender(alice.address)
.withValue(0);

// addition()
let additionVarTransaction = additionVarInteraction
.withSender(alice.address)
.useThenIncrementNonceOf(alice.account)
.buildTransaction();

// returnEgld()
await signTransaction({ transaction: returnEgldTransaction, wallet: alice });
let { bundle: bundleEgld } = await controller.execute(returnEgldInteraction, returnEgldTransaction);
assert.isTrue(bundleEgld.returnCode.equals(ReturnCode.Ok));
assert.lengthOf(bundleEgld.values, 1);
assert.deepEqual(bundleEgld.values[0], new ManagedDecimalValue(1, 18));

// addition with const decimals()
await signTransaction({ transaction: additionTransaction, wallet: alice });
let { bundle: bundleAdditionConst } = await controller.execute(additionInteraction, additionTransaction);
assert.isTrue(bundleAdditionConst.returnCode.equals(ReturnCode.Ok));
assert.lengthOf(bundleAdditionConst.values, 1);
assert.deepEqual(bundleAdditionConst.values[0], new ManagedDecimalValue(5, 2));

// log
await signTransaction({ transaction: mdLnTransaction, wallet: alice });
let { bundle: bundleMDLn } = await controller.execute(mdLnInteraction, mdLnTransaction);
assert.isTrue(bundleMDLn.returnCode.equals(ReturnCode.Ok));
assert.lengthOf(bundleMDLn.values, 1);
assert.deepEqual(bundleMDLn.values[0], new ManagedDecimalSignedValue(3.135553845, 9));

// addition with var decimals
await signTransaction({ transaction: additionVarTransaction, wallet: alice });
let { bundle: bundleAddition } = await controller.execute(additionVarInteraction, additionVarTransaction);
assert.isTrue(bundleAddition.returnCode.equals(ReturnCode.Ok));
assert.lengthOf(bundleAddition.values, 1);
assert.deepEqual(bundleAddition.values[0], new ManagedDecimalValue(new BigNumber(6254154138880), 9));
});

it("should interact with 'counter' (local testnet)", async function () {
this.timeout(120000);

Expand Down
43 changes: 43 additions & 0 deletions src/smartcontracts/nativeSerializer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
EndpointModifiers,
EndpointParameterDefinition,
ListType,
ManagedDecimalType,
ManagedDecimalValue,
NullType,
OptionalType,
OptionalValue,
Expand Down Expand Up @@ -401,6 +403,47 @@ describe("test native serializer", () => {
]);
});

it("should accept managed decimals with constants and variable decimals", async () => {
const endpoint = AbiRegistry.create({
endpoints: [
{
name: "foo",
inputs: [
{
type: "ManagedDecimal<8>",
},
{
type: "ManagedDecimal<usize>",
},
],
outputs: [],
},
],
}).getEndpoint("foo");

// Pass only native values
let typedValues = NativeSerializer.nativeToTypedValues(
[
[2, 8],
[12.5644, 6],
],
endpoint,
);

assert.deepEqual(typedValues[0].getType(), new ManagedDecimalType(8));
assert.deepEqual(typedValues[0].valueOf(), new BigNumber(2));
assert.deepEqual(typedValues[1].getType(), new ManagedDecimalType("usize"));
assert.deepEqual(typedValues[1].valueOf(), new BigNumber(12.5644));

// Pass a mix of native and typed values
typedValues = NativeSerializer.nativeToTypedValues([new ManagedDecimalValue(2, 8), [12.5644, 6]], endpoint);

assert.deepEqual(typedValues[0].getType(), new ManagedDecimalType(8));
assert.deepEqual(typedValues[0].valueOf(), new BigNumber(2));
assert.deepEqual(typedValues[1].getType(), new ManagedDecimalType("usize"));
assert.deepEqual(typedValues[1].valueOf(), new BigNumber(12.5644));
});

it("should accept no value for variadic types", async () => {
const endpoint = AbiRegistry.create({
endpoints: [
Expand Down
Loading
Loading