Skip to content

Commit

Permalink
feat: contract factory deploy arguments
Browse files Browse the repository at this point in the history
  • Loading branch information
irisdv committed Dec 9, 2022
1 parent 65b9ea5 commit 9eff7f4
Show file tree
Hide file tree
Showing 5 changed files with 288 additions and 235 deletions.
16 changes: 14 additions & 2 deletions __tests__/contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,12 +223,24 @@ describe('class ContractFactory {}', () => {
});
test('deployment of new contract', async () => {
const factory = new ContractFactory(compiledErc20, classHash, account);
const erc20 = await factory.deploy(constructorCalldata);
const erc20 = await factory.deploy(
compileCalldata({
name: encodeShortString('Token'),
symbol: encodeShortString('ERC20'),
recipient: wallet,
})
);
expect(erc20 instanceof Contract);
});
test('wait for deployment transaction', async () => {
const factory = new ContractFactory(compiledErc20, classHash, account);
const contract = await factory.deploy(constructorCalldata);
const contract = await factory.deploy(
compileCalldata({
name: encodeShortString('Token'),
symbol: encodeShortString('ERC20'),
recipient: wallet,
})
);
expect(contract.deployed()).resolves.not.toThrow();
});
test('attach new contract', async () => {
Expand Down
1 change: 0 additions & 1 deletion src/account/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,6 @@ export class Account extends Provider implements AccountInterface {
return { declare: { ...declare, class_hash: classHash }, deploy };
}


public async deployAccount(
{
classHash,
Expand Down
19 changes: 13 additions & 6 deletions src/contract/contractFactory.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import assert from 'minimalistic-assert';

import { AccountInterface } from '../account';
import { Abi, CompiledContract, RawArgs } from '../types';
import { Abi, CompiledContract, FunctionAbi } from '../types';
import { CheckCallData } from '../utils/calldata';
import { Contract } from './default';

export class ContractFactory {
Expand All @@ -13,6 +14,8 @@ export class ContractFactory {

account: AccountInterface;

private checkCalldata: CheckCallData;

constructor(
compiledContract: CompiledContract,
classHash: string,
Expand All @@ -23,19 +26,23 @@ export class ContractFactory {
this.compiledContract = compiledContract;
this.account = account;
this.classHash = classHash;
this.checkCalldata = new CheckCallData(abi);
}

/**
* Deploys contract and returns new instance of the Contract
*
* @param constructorCalldata - Constructor Calldata
* @param args - Array of the constructor arguments for deployment
* @param addressSalt (optional) - Address Salt for deployment
* @returns deployed Contract
*/
public async deploy(
constructorCalldata?: RawArgs,
addressSalt?: string | undefined
): Promise<Contract> {
public async deploy(args: Array<any> = [], addressSalt?: string | undefined): Promise<Contract> {
this.checkCalldata.validateMethodAndArgs('DEPLOY', 'constructor', args);
const { inputs } = this.abi.find((abi) => abi.type === 'constructor') as FunctionAbi;

// compile calldata
const constructorCalldata = this.checkCalldata.compileCalldata(args, inputs);

const {
deploy: { contract_address, transaction_hash },
} = await this.account.declareDeploy({
Expand Down
237 changes: 11 additions & 226 deletions src/contract/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
AsyncContractFunction,
BlockTag,
Call,
Calldata,
ContractFunction,
FunctionAbi,
InvokeFunctionResponse,
Expand All @@ -20,7 +19,8 @@ import {
Result,
StructAbi,
} from '../types';
import { BigNumberish, toBN, toFelt } from '../utils/number';
import { CheckCallData } from '../utils/calldata';
import { BigNumberish, toBN } from '../utils/number';
import { ContractInterface } from './interface';

function parseFelt(candidate: string): BN {
Expand Down Expand Up @@ -122,6 +122,8 @@ export class Contract implements ContractInterface {

readonly [key: string]: AsyncContractFunction | any;

private checkCalldata: CheckCallData;

/**
* Contract class to handle contract methods
*
Expand All @@ -146,6 +148,7 @@ export class Contract implements ContractInterface {
}),
{}
);
this.checkCalldata = new CheckCallData(abi);

Object.defineProperty(this, 'functions', {
enumerable: true,
Expand Down Expand Up @@ -242,11 +245,11 @@ export class Contract implements ContractInterface {
assert(this.address !== null, 'contract is not connected to an address');

// validate method and args
this.validateMethodAndArgs('CALL', method, args);
this.checkCalldata.validateMethodAndArgs('CALL', method, args);
const { inputs } = this.abi.find((abi) => abi.name === method) as FunctionAbi;

// compile calldata
const calldata = this.compileCalldata(args, inputs);
const calldata = this.checkCalldata.compileCalldata(args, inputs);
return this.providerOrAccount
.callContract(
{
Expand All @@ -267,7 +270,7 @@ export class Contract implements ContractInterface {
// ensure contract is connected
assert(this.address !== null, 'contract is not connected to an address');
// validate method and args
this.validateMethodAndArgs('INVOKE', method, args);
this.checkCalldata.validateMethodAndArgs('INVOKE', method, args);

const { inputs } = this.abi.find((abi) => abi.name === method) as FunctionAbi;
const inputsLength = inputs.reduce((acc, input) => {
Expand All @@ -283,7 +286,7 @@ export class Contract implements ContractInterface {
);
}
// compile calldata
const calldata = this.compileCalldata(args, inputs);
const calldata = this.checkCalldata.compileCalldata(args, inputs);

const invocation = {
contractAddress: this.address,
Expand Down Expand Up @@ -320,7 +323,7 @@ export class Contract implements ContractInterface {
assert(this.address !== null, 'contract is not connected to an address');

// validate method and args
this.validateMethodAndArgs('INVOKE', method, args);
this.checkCalldata.validateMethodAndArgs('INVOKE', method, args);
const invocation = this.populateTransaction[method](...args);
if ('estimateInvokeFee' in this.providerOrAccount) {
return this.providerOrAccount.estimateInvokeFee(invocation);
Expand All @@ -333,167 +336,10 @@ export class Contract implements ContractInterface {
return {
contractAddress: this.address,
entrypoint: method,
calldata: this.compileCalldata(args, inputs),
calldata: this.checkCalldata.compileCalldata(args, inputs),
};
}

/**
* Deep parse of the object that has been passed to the method
*
* @param struct - struct that needs to be calculated
* @return {number} - number of members for the given struct
*/
private calculateStructMembers(struct: string): number {
return this.structs[struct].members.reduce((acc, member) => {
if (member.type === 'felt') {
return acc + 1;
}
return acc + this.calculateStructMembers(member.type);
}, 0);
}

/**
* Validates if all arguments that are passed to the method are corresponding to the ones in the abi
*
* @param type - type of the method
* @param method - name of the method
* @param args - arguments that are passed to the method
*/
protected validateMethodAndArgs(type: 'INVOKE' | 'CALL', method: string, args: Array<any> = []) {
// ensure provided method exists
const invokeableFunctionNames = this.abi
.filter((abi) => {
if (abi.type !== 'function') return false;
const isView = abi.stateMutability === 'view';
return type === 'INVOKE' ? !isView : isView;
})
.map((abi) => abi.name);
assert(
invokeableFunctionNames.includes(method),
`${type === 'INVOKE' ? 'invokeable' : 'viewable'} method not found in abi`
);

// ensure args match abi type
const methodAbi = this.abi.find(
(abi) => abi.name === method && abi.type === 'function'
) as FunctionAbi;
let argPosition = 0;
methodAbi.inputs.forEach((input) => {
if (/_len$/.test(input.name)) {
return;
}
if (input.type === 'felt') {
assert(
typeof args[argPosition] === 'string' ||
typeof args[argPosition] === 'number' ||
args[argPosition] instanceof BN,
`arg ${input.name} should be a felt (string, number, BigNumber)`
);
argPosition += 1;
} else if (input.type in this.structs && typeof args[argPosition] === 'object') {
if (Array.isArray(args[argPosition])) {
const structMembersLength = this.calculateStructMembers(input.type);
assert(
args[argPosition].length === structMembersLength,
`arg should be of length ${structMembersLength}`
);
} else {
this.structs[input.type].members.forEach(({ name }) => {
assert(
Object.keys(args[argPosition]).includes(name),
`arg should have a property ${name}`
);
});
}
argPosition += 1;
} else {
assert(Array.isArray(args[argPosition]), `arg ${input.name} should be an Array`);
if (input.type === 'felt*') {
args[argPosition].forEach((felt: BigNumberish) => {
assert(
typeof felt === 'string' || typeof felt === 'number' || felt instanceof BN,
`arg ${input.name} should be an array of string, number or BigNumber`
);
});
argPosition += 1;
} else if (/\(felt/.test(input.type)) {
const tupleLength = input.type.split(',').length;
assert(
args[argPosition].length === tupleLength,
`arg ${input.name} should have ${tupleLength} elements in tuple`
);
args[argPosition].forEach((felt: BigNumberish) => {
assert(
typeof felt === 'string' || typeof felt === 'number' || felt instanceof BN,
`arg ${input.name} should be an array of string, number or BigNumber`
);
});
argPosition += 1;
} else {
const arrayType = input.type.replace('*', '');
args[argPosition].forEach((struct: any) => {
this.structs[arrayType].members.forEach(({ name }) => {
if (Array.isArray(struct)) {
const structMembersLength = this.calculateStructMembers(arrayType);
assert(
struct.length === structMembersLength,
`arg should be of length ${structMembersLength}`
);
} else {
assert(
Object.keys(struct).includes(name),
`arg ${input.name} should be an array of ${arrayType}`
);
}
});
});
argPosition += 1;
}
}
});
}

/**
* Deep parse of the object that has been passed to the method
*
* @param element - element that needs to be parsed
* @param type - name of the method
* @return {string | string[]} - parsed arguments in format that contract is expecting
*/

protected parseCalldataValue(
element: ParsedStruct | BigNumberish | BigNumberish[],
type: string
): string | string[] {
if (element === undefined) {
throw Error('Missing element in calldata');
}
if (Array.isArray(element)) {
const structMemberNum = this.calculateStructMembers(type);
if (element.length !== structMemberNum) {
throw Error('Missing element in calldata');
}
return element.map((el) => toFelt(el));
}
// checking if the passed element is struct or element in struct
if (this.structs[type] && this.structs[type].members.length) {
// going through all the members of the struct and parsing the value
return this.structs[type].members.reduce((acc, member: AbiEntry) => {
// if the member of the struct is another struct this will return array of the felts if not it will be single felt
// TODO: refactor types so member name can be used as keyof ParsedStruct
/* @ts-ignore */
const parsedData = this.parseCalldataValue(element[member.name], member.type);
if (typeof parsedData === 'string') {
acc.push(parsedData);
} else {
acc.push(...parsedData);
}
return acc;
}, [] as string[]);
}
return toFelt(element as BigNumberish);
}

/**
* Parse of the response elements that are converted to Object (Struct) by using the abi
*
Expand All @@ -516,67 +362,6 @@ export class Contract implements ContractInterface {
return parseFelt(responseIterator.next().value);
}

/**
* Parse one field of the calldata by using input field from the abi for that method
*
* @param args - value of the field
* @param input - input(field) information from the abi that will be used to parse the data
* @return {string | string[]} - parsed arguments in format that contract is expecting
*/
protected parseCalldataField(argsIterator: Iterator<any>, input: AbiEntry): string | string[] {
const { name, type } = input;
const { value } = argsIterator.next();

const parsedCalldata: string[] = [];
switch (true) {
case /\*/.test(type):
if (Array.isArray(value)) {
parsedCalldata.push(toFelt(value.length));
return (value as (BigNumberish | ParsedStruct)[]).reduce((acc, el) => {
if (/felt/.test(type)) {
acc.push(toFelt(el as BigNumberish));
} else {
acc.push(...this.parseCalldataValue(el, type.replace('*', '')));
}
return acc;
}, parsedCalldata);
}
throw Error(`Expected ${name} to be array`);
case type in this.structs:
return this.parseCalldataValue(value as ParsedStruct | BigNumberish[], type);
case /\(felt/.test(type):
if (Array.isArray(value)) {
return value.map((el) => toFelt(el as BigNumberish));
}
throw Error(`Expected ${name} to be array`);
default:
return toFelt(value as BigNumberish);
}
}

/**
* Parse the calldata by using input fields from the abi for that method
*
* @param args - arguments passed the the method
* @param inputs - list of inputs(fields) that are in the abi
* @return {Calldata} - parsed arguments in format that contract is expecting
*/
protected compileCalldata(args: Array<any>, inputs: AbiEntry[]): Calldata {
const argsIterator = args[Symbol.iterator]();
return inputs.reduce((acc, input) => {
if (/_len$/.test(input.name)) {
return acc;
}
const parsedData = this.parseCalldataField(argsIterator, input);
if (Array.isArray(parsedData)) {
acc.push(...parsedData);
} else {
acc.push(parsedData);
}
return acc;
}, [] as Calldata);
}

/**
* Parse elements of the response and structuring them into one field by using output property from the abi for that method
*
Expand Down
Loading

0 comments on commit 9eff7f4

Please sign in to comment.