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

Allow users to connect to existing contracts - closes #46 #49

Merged
merged 8 commits into from
Oct 8, 2023
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,7 @@ typings/
*~
\#*
.\#*

# Artifacts
test/fixture-projects/hardhat-project/artifacts

9 changes: 9 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
2023-10-06 Richard Watts <[email protected]>

* index.ts: hre.interactWithScillaContract(address) - allows you to interact with an already-deployed Scilla contract.

2023-10-03 Richard Watts <[email protected]>

* index.ts: A new hre.setScillaDefaults() allowing you to change the default gasPrice, gasLimit, timeout and attempts
* ZilliqaHardhatObject.ts: getDefaultAccount() allows you to retrieve the default signing account.
* test/deploy.live.test.ts: Test that we can actually deploy contracts.
192 changes: 24 additions & 168 deletions src/ScillaContractDeployer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Zilliqa } from "@zilliqa-js/zilliqa";
import fs from "fs";
import { HardhatPluginError } from "hardhat/plugins";
import { HardhatRuntimeEnvironment } from "hardhat/types";

import * as ScillaContractProxy from "./ScillaContractProxy";
import { ContractInfo } from "./ScillaContractsInfoUpdater";
import {
Field,
Expand All @@ -17,7 +17,8 @@ import {
TransitionParam,
} from "./ScillaParser";

interface Value {

export interface Value {
vname: string;
type: string;
value: string;
Expand All @@ -38,6 +39,7 @@ export interface Setup {
accounts: Account[];
}


export let setup: Setup | null = null;

// The optional params are listed in popularity order.
Expand Down Expand Up @@ -76,6 +78,24 @@ function read(f: string) {
return t;
}

/// Allows you to change setup parameters. Available params:
/// gasPrice, gasLimit, attempts, timeout.
export function updateSetup(args: any) {
if (setup === null) {
throw new HardhatPluginError("hardhat-scilla-plugin", "Please call the initZilliqa function.");
}
let newSetup : Setup = {
sal-zilliqa marked this conversation as resolved.
Show resolved Hide resolved
zilliqa: setup.zilliqa,
version: setup.version,
gasPrice: args.gasPrice ? units.toQa(args.gasPrice.toString(), units.Units.Li) : setup.gasPrice,
gasLimit: args.gasLimit ? Long.fromNumber(args.gasLimit) : setup.gasLimit,
attempts: args.attempts ?? setup.attempts,
timeout: args.timeout ?? setup.timeout,
accounts : setup.accounts
};
setup = newSetup;
}

export function setAccount(account: number | Account) {
if (setup === null) {
throw new HardhatPluginError(
Expand Down Expand Up @@ -103,77 +123,6 @@ declare module "@zilliqa-js/contract" {

export type ScillaContract = Contract;

function handleParam(param: Field, arg: any): Value {
if (typeof param.typeJSON === "undefined") {
throw new HardhatPluginError(
"hardhat-scilla-plugin",
"Parameters were incorrectly parsed. Try clearing your scilla.cache file."
);
} else if (typeof param.typeJSON === "string") {
return {
vname: param.name,
type: param.type,
value: arg.toString(),
};
} else {
const values: Value[] = [];
param.typeJSON.argtypes.forEach((param: Field, index: number) => {
values.push(handleUnnamedParam(param, arg[index]));
});
const argtypes = param.typeJSON.argtypes.map((x) => x.type);
/*
We use JSON.parse(JSON.strigify()) because we need to create a JSON with a constructor
field. Typescript expects this constructor to have the same type as an object
constructor which is not possible as it should be a string for our purposes. This trick
allows forces the typescript compiler to enforce this.
*/
const value = JSON.parse(
JSON.stringify({
constructor: param.typeJSON.ctor,
argtypes,
arguments: values,
})
);
return {
vname: param.name,
type: param.type,
value,
};
}
}

function handleUnnamedParam(param: Field, arg: any): Value {
if (typeof param.typeJSON === "undefined") {
throw new HardhatPluginError(
"hardhat-scilla-plugin",
"Parameters were incorrectly parsed. Try clearing your scilla.cache file."
);
} else if (typeof param.typeJSON === "string") {
return arg.toString();
} else {
const values: Value[] = [];
param.typeJSON.argtypes.forEach((param: Field, index: number) => {
values.push(handleUnnamedParam(param, arg[index]));
});
const argtypes = param.typeJSON.argtypes.map((x) => x.type);
/*
We use JSON.parse(JSON.strigify()) because we need to create a JSON with a constructor
field. Typescript expects this constructor to have the same type as an object
constructor which is not possible as it should be a string for our purposes. This trick
allows forces the typescript compiler to enforce this.
*/
return JSON.parse(
JSON.stringify({
vname: param.name,
type: param.type,
constructor: param.typeJSON.ctor,
argtypes,
arguments: values,
})
);
}
}

export interface UserDefinedLibrary {
name: string;
address: string;
Expand Down Expand Up @@ -209,63 +158,7 @@ export async function deploy(
[tx, sc] = await deployFromFile(contractInfo.path, init, txParamsForContractDeployment);
sc.deployed_by = tx;

contractInfo.parsedContract.transitions.forEach((transition) => {
sc[transition.name] = async (...args: any[]) => {
let callParams: CallParams = {
version: setup!.version,
gasPrice: setup!.gasPrice,
gasLimit: setup!.gasLimit,
amount: new BN(0),
};

if (args.length === transition.params.length + 1) {
// The last param is Tx info such as amount
const txParams = args.pop();
callParams = { ...callParams, ...txParams };
} else if (args.length !== transition.params.length) {
throw new Error(
`Expected to receive ${transition.params.length} parameters for ${transition.name} but got ${args.length}`
);
}

const values: Value[] = [];
transition.params.forEach((param: TransitionParam, index: number) => {
values.push(handleParam(param, args[index]));
});

return sc_call(sc, transition.name, values, callParams);
};
});

contractInfo.parsedContract.fields.forEach((field) => {
sc[field.name] = async () => {
const state = await sc.getState();
if (isNumeric(field.type)) {
return Number(state[field.name]);
}
return state[field.name];
};
});

if (contractInfo.parsedContract.constructorParams) {
contractInfo.parsedContract.constructorParams.forEach((field) => {
sc[field.name] = async () => {
const states: State = await sc.getInit();
const state = states.filter(
(s: { vname: string }) => s.vname === field.name
)[0];

if (isNumeric(field.type)) {
return Number(state.value);
}
return state.value;
};
});
}

// Will shadow any transition named ctors. But done like this to avoid changing the signature of deploy.
const parsedCtors = contractInfo.parsedContract.ctors;
sc.ctors = generateTypeConstructors(parsedCtors);
ScillaContractProxy.injectProxies(setup!, contractInfo, sc);

return sc;
}
Expand Down Expand Up @@ -387,46 +280,9 @@ export async function deployFromFile(
);

// Let's add this function for further signer/executer changes.
sc.connect = (signer: Account) => {
sc.executer = signer;

// If account is not added already, add it to the list.
if (setup?.accounts.findIndex((acc) => acc.privateKey === signer.privateKey) === -1) {
setup?.zilliqa.wallet.addByPrivateKey(signer.privateKey);
setup?.accounts.push(signer);
}
return sc;
}
ScillaContractProxy.injectConnectors(setup, sc);

sc.connect(deployer);

return [tx, sc];
}

// call a smart contract's transition with given args and an amount to send from a given public key
export async function sc_call(
sc: ScillaContract,
transition: string,
args: Value[] = [],
callParams: CallParams
) {
if (setup === null) {
throw new HardhatPluginError(
"hardhat-scilla-plugin",
"Please call initZilliqa function."
);
}

if (callParams.pubKey === undefined && sc.executer) {
callParams.pubKey = sc.executer.publicKey;
}

return sc.call(
transition,
args,
callParams,
setup.attempts,
setup.timeout,
true
);
}
67 changes: 67 additions & 0 deletions src/ScillaContractInteractor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Account, Transaction } from "@zilliqa-js/account";
import { CallParams, Contract, Init, State } from "@zilliqa-js/contract";
import { BN, bytes, Long, units } from "@zilliqa-js/util";
import { Zilliqa } from "@zilliqa-js/zilliqa";
import fs from "fs";
import { HardhatPluginError } from "hardhat/plugins";
import { HardhatRuntimeEnvironment } from "hardhat/types";
import * as ScillaContractProxy from "./ScillaContractProxy";
import { ContractInfo } from "./ScillaContractsInfoUpdater";
import { ScillaContract, Value } from "./ScillaContractDeployer";
import * as ScillaContractDeployer from "./ScillaContractDeployer";
import * as ScillaContractsInfoUpdater from "./ScillaContractsInfoUpdater";
import { createHash } from "crypto";
import {
ContractName,
ParsedContract,
parseScilla,
parseScillaLibrary,
} from "./ScillaParser";
import {
Field,
Fields,
generateTypeConstructors,
isNumeric,
TransitionParam,
} from "./ScillaParser";
import * as ZilliqaUtils from './ZilliqaUtils';

export async function contractFromAddress(hre: HardhatRuntimeEnvironment, address: string) : Promise<ScillaContract | undefined> {
// Fetch the code from the blockchain
if (ScillaContractDeployer.setup === null) {
throw new HardhatPluginError("hardhat-scilla-plugin", "Please call the initZilliqa function.");
}
let setup = ScillaContractDeployer.setup
let zilliqa = setup.zilliqa
let codeResult = await zilliqa.blockchain.getSmartContractCode(address);
if (codeResult !== undefined && codeResult.result !== undefined
&& codeResult.result.code !== undefined) {
let codeText = codeResult.result.code;
// Now parse it. Sadly, need a file for this. Even more sadly,m
sal-zilliqa marked this conversation as resolved.
Show resolved Hide resolved
let tempFile = ZilliqaUtils.createTemporaryFile('contract', 'scilla');
fs.writeFileSync( tempFile, codeText );
let parsed = await parseScilla(tempFile);
let hash = ScillaContractsInfoUpdater.getFileHash(tempFile);
let contractInfo : ContractInfo = {
hash: hash,
path: address,
parsedContract: parsed
};

ZilliqaUtils.deleteTemporaryFile(tempFile);
// OK. Now I need to create a contract factory ..
// We don't actually need the ABI
let contract = zilliqa.contracts.at( address, undefined );
// Now we can fill the proxies in.
ScillaContractProxy.injectConnectors(setup, contract);
// Set the default deployer
const deployer = setup.zilliqa.wallet.defaultAccount ?? setup.accounts[0];
contract.connect(deployer);

// Now build a ContractInfo
ScillaContractProxy.injectProxies(setup!, contractInfo, contract);
return contract;
}

return undefined;
}
Loading
Loading