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 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
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 👍

}
}
9 changes: 6 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,12 @@ export class TypeFormulaParser {
private acquireTypeWithParameters(stack: any[]): TypeFormula {
const typeParameters = this.acquireTypeParameters(stack);
const typeName = stack.pop();

if (typeName === "ManagedDecimal" || typeName === "ManagedDecimalSigned") {
const metadata = typeParameters[0].name;
const typeFormula = new TypeFormula(typeName, [], metadata);
return typeFormula;
}
const typeFormula = new TypeFormula(typeName, typeParameters.reverse());
return typeFormula;
}
Expand All @@ -92,7 +96,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
56 changes: 56 additions & 0 deletions src/smartcontracts/codec/managedDecimal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import BigNumber from "bignumber.js";
import { BigUIntValue, ManagedDecimalType, ManagedDecimalValue, U32Value } from "../typesystem";
import { BinaryCodec } from "./binary";
import { bufferToBigInt } from "./utils";
import { SizeOfU32 } from "./constants";

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);
return [result, length];
}

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

if (type.isVariable()) {
const bigUintSize = buffer.length - SizeOfU32;

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

return new ManagedDecimalValue(value, scale);
}

const value = bufferToBigInt(buffer);
const metadata = type.getMetadata();
const scale = typeof metadata === "number" ? metadata : 0;
return new ManagedDecimalValue(value, scale);
}

encodeNested(value: ManagedDecimalValue): Buffer {
let buffers: Buffer[] = [];
if (value.isVariable()) {
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(23000000000, 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(3135553845, 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