Skip to content

Commit

Permalink
Merge pull request #477 from multiversx/TOOL-223-add-managed-decimal-…
Browse files Browse the repository at this point in the history
…support-from-rust

Tool 223 add managed decimal support from rust
  • Loading branch information
danielailie authored Sep 10, 2024
2 parents e7b8fa0 + 3838dc7 commit b8927c6
Show file tree
Hide file tree
Showing 19 changed files with 6,723 additions and 32 deletions.
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;
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(", ")}>`
: "";
const baseName = `${this.name}${typeParameters}`;

return this.metadata !== undefined ? `${baseName}*${this.metadata}*` : baseName;
}
}
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 () {
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

0 comments on commit b8927c6

Please sign in to comment.