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

Integrate the SC Intents Factory in the SmartContract Class #330

Merged
merged 7 commits into from
Sep 25, 2023
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: 17 additions & 0 deletions src/smartcontracts/codeMetadata.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { assert } from "chai";
import { CodeMetadata } from "./codeMetadata";

describe("test code metadata", function () {
it("should test code metadata from bytes", () => {
const bytes = new Uint8Array([1, 0]);
const codeMetadata = CodeMetadata.fromBytes(bytes);

assert.equal(codeMetadata.toString(), "0100");
assert.deepEqual(codeMetadata.toJSON(), {
upgradeable: true,
readable: false,
payable: false,
payableBySc: false
});
});
});
19 changes: 18 additions & 1 deletion src/smartcontracts/codeMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export class CodeMetadata {
private readable: boolean;
private payable: boolean;
private payableBySc: boolean;
private static readonly codeMetadataLength = 2;

/**
* Creates a metadata object. By default, set the `upgradeable` attribute, and uset all others.
Expand All @@ -22,6 +23,22 @@ export class CodeMetadata {
this.payableBySc = payableBySc
}

static fromBytes(bytes: Uint8Array): CodeMetadata {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should be unit tested.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

added unit test.

if (bytes.length !== this.codeMetadataLength) {
return new CodeMetadata();
}
Comment on lines +27 to +29
Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps raising an exception would have worked 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.

Well, I've done it how it's done in vm-common.


const byteZero = bytes[0];
const byteOne = bytes[1];

const upgradeable = (byteZero & ByteZero.Upgradeable) !== 0;
const readable = (byteZero & ByteZero.Readable) !== 0;
const payable = (byteOne & ByteOne.Payable) !== 0;
const payableBySc = (byteOne & ByteOne.PayableBySc) !== 0;

return new CodeMetadata(upgradeable, readable, payable, payableBySc);
}

/**
* Adjust the metadata (the `upgradeable` attribute), when preparing the deployment transaction.
*/
Expand Down Expand Up @@ -49,7 +66,7 @@ export class CodeMetadata {
togglePayableBySc(value: boolean) {
this.payableBySc = value;
}

/**
* Converts the metadata to the protocol-friendly representation.
*/
Expand Down
6 changes: 3 additions & 3 deletions src/smartcontracts/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export interface ISmartContract {
export interface DeployArguments {
code: ICode;
codeMetadata?: ICodeMetadata;
initArguments?: TypedValue[];
initArguments?: any[];
Copy link
Contributor

Choose a reason for hiding this comment

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

Indeed. Not a breaking change.

value?: ITransactionValue;
gasLimit: IGasLimit;
gasPrice?: IGasPrice;
Expand All @@ -42,7 +42,7 @@ export interface DeployArguments {
export interface UpgradeArguments {
code: ICode;
codeMetadata?: ICodeMetadata;
initArguments?: TypedValue[];
initArguments?: any[];
value?: ITransactionValue;
gasLimit: IGasLimit;
gasPrice?: IGasPrice;
Expand All @@ -52,7 +52,7 @@ export interface UpgradeArguments {

export interface CallArguments {
func: IContractFunction;
args?: TypedValue[];
args?: any[];
value?: ITransactionValue;
gasLimit: IGasLimit;
receiver?: IAddress;
Expand Down
39 changes: 39 additions & 0 deletions src/smartcontracts/smartContract.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,4 +125,43 @@ describe("test contract", () => {
assert.isTrue((await provider.getTransactionStatus(hashOne)).isExecuted());
assert.isTrue((await provider.getTransactionStatus(hashTwo)).isExecuted());
});

it("should upgrade", async () => {
setupUnitTestWatcherTimeouts();
let watcher = new TransactionWatcher(provider);

let contract = new SmartContract();
contract.setAddress(Address.fromBech32("erd1qqqqqqqqqqqqqpgq3ytm9m8dpeud35v3us20vsafp77smqghd8ss4jtm0q"))

let deployTransaction = contract.upgrade({
code: Code.fromBuffer(Buffer.from([1, 2, 3, 4])),
gasLimit: 1000000,
chainID: chainID,
caller: alice.address
});

provider.mockUpdateAccount(alice.address, account => {
account.nonce = 42;
});

await alice.sync(provider);
deployTransaction.setNonce(alice.account.nonce);

assert.equal(deployTransaction.getData().valueOf().toString(), "upgradeContract@01020304@0100");
assert.equal(deployTransaction.getGasLimit().valueOf(), 1000000);
assert.equal(deployTransaction.getNonce().valueOf(), 42);

// Sign the transaction
alice.signer.sign(deployTransaction);

// Now let's broadcast the deploy transaction, and wait for its execution.
let hash = await provider.sendTransaction(deployTransaction);

await Promise.all([
provider.mockTransactionTimeline(deployTransaction, [new Wait(40), new TransactionStatus("pending"), new Wait(40), new TransactionStatus("executed"), new MarkCompleted()]),
watcher.awaitCompleted(deployTransaction)
]);

assert.isTrue((await provider.getTransactionStatus(hash)).isExecuted());
});
});
132 changes: 92 additions & 40 deletions src/smartcontracts/smartContract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,19 @@ import { bigIntToBuffer } from "./codec/utils";
import { CodeMetadata } from "./codeMetadata";
import { ContractFunction } from "./function";
import { Interaction } from "./interaction";
import { CallArguments, DeployArguments, ISmartContract, QueryArguments, UpgradeArguments } from "./interface";
import { CallArguments, DeployArguments, ICodeMetadata, ISmartContract, QueryArguments, UpgradeArguments } from "./interface";
import { NativeSerializer } from "./nativeSerializer";
import { Query } from "./query";
import { ArwenVirtualMachine, ContractCallPayloadBuilder, ContractDeployPayloadBuilder, ContractUpgradePayloadBuilder } from "./transactionPayloadBuilders";
import { ArwenVirtualMachine, ContractCallPayloadBuilder, ContractUpgradePayloadBuilder } from "./transactionPayloadBuilders";
import { EndpointDefinition, TypedValue } from "./typesystem";
import { SmartContractTransactionIntentsFactory } from "../transactionIntentsFactories/smartContractTransactionIntentsFactory";
import { TransactionIntentsFactoryConfig } from "../transactionIntentsFactories/transactionIntentsFactoryConfig";
import { TransactionPayload } from "../transactionPayload";
const createKeccakHash = require("keccak");

interface IAbi {
constructorDefinition: EndpointDefinition;
Copy link
Contributor

Choose a reason for hiding this comment

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

Theoretically, a breaking change. In practice, it's not (generally, speaking, the users will always pass a AbiRegistry here). However, for safety, we can also declare it as 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.

We can't make constructorDefinition optional since the SmartContractTransactionIntensFactory needs that field.

Copy link
Contributor

Choose a reason for hiding this comment

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

It's all right as it is. Practically (99.99%) not a breaking change for any client code.


getEndpoints(): EndpointDefinition[];
getEndpoint(name: string | ContractFunction): EndpointDefinition;
}
Expand Down Expand Up @@ -110,27 +115,58 @@ export class SmartContract implements ISmartContract {
deploy({ deployer, code, codeMetadata, initArguments, value, gasLimit, gasPrice, chainID }: DeployArguments): Transaction {
Compatibility.guardAddressIsSetAndNonZero(deployer, "'deployer' of SmartContract.deploy()", "pass the actual address to deploy()");

codeMetadata = codeMetadata || new CodeMetadata();
initArguments = initArguments || [];
value = value || 0;
const config = new TransactionIntentsFactoryConfig(chainID.valueOf());
const scIntentFactory = new SmartContractTransactionIntentsFactory({
config: config,
abi: this.abi
});

let payload = new ContractDeployPayloadBuilder()
.setCode(code)
.setCodeMetadata(codeMetadata)
.setInitArgs(initArguments)
.build();
const bytecode = Buffer.from(code.toString(), 'hex');
const metadataAsJson = this.getMetadataPropertiesAsObject(codeMetadata);

let transaction = new Transaction({
receiver: Address.Zero(),
const intent = scIntentFactory.createTransactionIntentForDeploy({
sender: deployer,
bytecode: bytecode,
gasLimit: gasLimit.valueOf(),
args: initArguments,
isUpgradeable: metadataAsJson.upgradeable,
isReadable: metadataAsJson.readable,
isPayable: metadataAsJson.payable,
isPayableBySmartContract: metadataAsJson.payableBySc
});

return new Transaction({
receiver: Address.fromBech32(intent.receiver),
sender: Address.fromBech32(intent.sender),
value: value,
gasLimit: gasLimit,
gasLimit: new BigNumber(intent.gasLimit).toNumber(),
gasPrice: gasPrice,
data: payload,
data: new TransactionPayload(Buffer.from(intent.data!)),
chainID: chainID
});
}

return transaction;
private getMetadataPropertiesAsObject(codeMetadata?: ICodeMetadata): {
upgradeable: boolean,
readable: boolean,
payable: boolean,
payableBySc: boolean
} {
let metadata: CodeMetadata;
if (codeMetadata) {
metadata = CodeMetadata.fromBytes(Buffer.from(codeMetadata.toString(), "hex"));
}
else {
metadata = new CodeMetadata();
}
const metadataAsJson = metadata.toJSON() as {
Copy link
Contributor

Choose a reason for hiding this comment

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

Alternatively, we can directly make the fields of CodeMetadata as public readonly:

https://github.com/multiversx/mx-sdk-js-core/blob/main/src/smartcontracts/codeMetadata.ts

Then use those, without an extra mapping to JSON.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not really good if someone uses the toggle... methods.

upgradeable: boolean,
readable: boolean,
payable: boolean,
payableBySc: boolean
};

return metadataAsJson;
}

/**
Expand All @@ -141,27 +177,36 @@ export class SmartContract implements ISmartContract {

this.ensureHasAddress();

codeMetadata = codeMetadata || new CodeMetadata();
initArguments = initArguments || [];
value = value || 0;
const config = new TransactionIntentsFactoryConfig(chainID.valueOf());
const scIntentFactory = new SmartContractTransactionIntentsFactory({
config: config,
abi: this.abi
});

let payload = new ContractUpgradePayloadBuilder()
.setCode(code)
.setCodeMetadata(codeMetadata)
.setInitArgs(initArguments)
.build();
const bytecode = Uint8Array.from(Buffer.from(code.toString(), 'hex'));
const metadataAsJson = this.getMetadataPropertiesAsObject(codeMetadata);

let transaction = new Transaction({
const intent = scIntentFactory.createTransactionIntentForUpgrade({
sender: caller,
receiver: this.getAddress(),
contract: this.getAddress(),
bytecode: bytecode,
gasLimit: gasLimit.valueOf(),
args: initArguments,
isUpgradeable: metadataAsJson.upgradeable,
isReadable: metadataAsJson.readable,
isPayable: metadataAsJson.payable,
isPayableBySmartContract: metadataAsJson.payableBySc
})

return new Transaction({
sender: Address.fromBech32(intent.sender),
receiver: Address.fromBech32(intent.receiver),
value: value,
gasLimit: gasLimit,
gasLimit: new BigNumber(intent.gasLimit).toNumber(),
gasPrice: gasPrice,
data: payload,
data: new TransactionPayload(Buffer.from(intent.data!)),
chainID: chainID
});

return transaction;
}

/**
Expand All @@ -172,25 +217,32 @@ export class SmartContract implements ISmartContract {

this.ensureHasAddress();

const config = new TransactionIntentsFactoryConfig(chainID.valueOf());
const scIntentFactory = new SmartContractTransactionIntentsFactory({
config: config,
abi: this.abi
});

args = args || [];
value = value || 0;

let payload = new ContractCallPayloadBuilder()
.setFunction(func)
.setArgs(args)
.build();
const intent = scIntentFactory.createTransactionIntentForExecute({
sender: caller,
contractAddress: receiver ? receiver : this.getAddress(),
functionName: func.toString(),
gasLimit: gasLimit.valueOf(),
args: args
})

let transaction = new Transaction({
return new Transaction({
sender: caller,
receiver: receiver ? receiver : this.getAddress(),
receiver: Address.fromBech32(intent.receiver),
value: value,
gasLimit: gasLimit,
gasLimit: new BigNumber(intent.gasLimit).toNumber(),
gasPrice: gasPrice,
data: payload,
chainID: chainID,
data: new TransactionPayload(Buffer.from(intent.data!)),
chainID: chainID
});

return transaction;
}

createQuery({ func, args, value, caller }: QueryArguments): Query {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,14 @@ describe("test smart contract intents factory", function () {
const deployIntent = factory.createTransactionIntentForExecute({
sender: sender,
contractAddress: contract,
func: func,
functionName: func,
gasLimit: gasLimit,
args: args
});
const abiDeployIntent = abiAwareFactory.createTransactionIntentForExecute({
sender: sender,
contractAddress: contract,
func: func,
functionName: func,
gasLimit: gasLimit,
args: args
});
Expand Down
Loading
Loading