Added a detailed description of renameOverloadedMethods
(special thanks to @fabioberger). Updated Javascript styles in the Abi-Gen and Utils packages, around support for function overloading.
This commit is contained in:
parent
61fc3346c2
commit
eecf09f515
@ -125,7 +125,7 @@ for (const abiFileName of abiFileNames) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const methodAbis = ABI.filter((abi: AbiDefinition) => abi.type === ABI_TYPE_METHOD) as MethodAbi[];
|
const methodAbis = ABI.filter((abi: AbiDefinition) => abi.type === ABI_TYPE_METHOD) as MethodAbi[];
|
||||||
const methodAbisSanitized = abiUtils.renameOverloadedMethods(methodAbis) as MethodAbi[];
|
const sanitizedMethodAbis = abiUtils.renameOverloadedMethods(methodAbis) as MethodAbi[];
|
||||||
const methodsData = _.map(methodAbis, (methodAbi, methodAbiIndex: number) => {
|
const methodsData = _.map(methodAbis, (methodAbi, methodAbiIndex: number) => {
|
||||||
_.forEach(methodAbi.inputs, (input, inputIndex: number) => {
|
_.forEach(methodAbi.inputs, (input, inputIndex: number) => {
|
||||||
if (_.isEmpty(input.name)) {
|
if (_.isEmpty(input.name)) {
|
||||||
@ -138,7 +138,7 @@ for (const abiFileName of abiFileNames) {
|
|||||||
...methodAbi,
|
...methodAbi,
|
||||||
singleReturnValue: methodAbi.outputs.length === 1,
|
singleReturnValue: methodAbi.outputs.length === 1,
|
||||||
hasReturnValue: methodAbi.outputs.length !== 0,
|
hasReturnValue: methodAbi.outputs.length !== 0,
|
||||||
tsName: methodAbisSanitized[methodAbiIndex].name,
|
tsName: sanitizedMethodAbis[methodAbiIndex].name,
|
||||||
functionSignature: abiUtils.getFunctionSignature(methodAbi),
|
functionSignature: abiUtils.getFunctionSignature(methodAbi),
|
||||||
};
|
};
|
||||||
return methodData;
|
return methodData;
|
||||||
|
@ -89,13 +89,10 @@ export class BaseContract {
|
|||||||
const methodAbis = this.abi.filter(
|
const methodAbis = this.abi.filter(
|
||||||
(abiDefinition: AbiDefinition) => abiDefinition.type === AbiType.Function,
|
(abiDefinition: AbiDefinition) => abiDefinition.type === AbiType.Function,
|
||||||
) as MethodAbi[];
|
) as MethodAbi[];
|
||||||
this._ethersInterfacesByFunctionSignature = _.transform(
|
this._ethersInterfacesByFunctionSignature = {};
|
||||||
methodAbis,
|
_.each(methodAbis, methodAbi => {
|
||||||
(result: EthersInterfaceByFunctionSignature, methodAbi) => {
|
|
||||||
const functionSignature = abiUtils.getFunctionSignature(methodAbi);
|
const functionSignature = abiUtils.getFunctionSignature(methodAbi);
|
||||||
result[functionSignature] = new ethersContracts.Interface([methodAbi]);
|
this._ethersInterfacesByFunctionSignature[functionSignature] = new ethersContracts.Interface([methodAbi]);
|
||||||
},
|
});
|
||||||
{},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,7 +45,7 @@ async function onDeployCommandAsync(argv: CliOptions): Promise<void> {
|
|||||||
const web3Wrapper = new Web3Wrapper(web3Provider);
|
const web3Wrapper = new Web3Wrapper(web3Provider);
|
||||||
const networkId = await web3Wrapper.getNetworkIdAsync();
|
const networkId = await web3Wrapper.getNetworkIdAsync();
|
||||||
const compilerOpts: CompilerOptions = {
|
const compilerOpts: CompilerOptions = {
|
||||||
contractDirs: getContractDirectoriesFromList(argv.contractsDir),
|
contractDirs: getContractDirectoriesFromList(argv.contractDirs),
|
||||||
networkId,
|
networkId,
|
||||||
optimizerEnabled: argv.shouldOptimize,
|
optimizerEnabled: argv.shouldOptimize,
|
||||||
artifactsDir: argv.artifactsDir,
|
artifactsDir: argv.artifactsDir,
|
||||||
|
@ -26,7 +26,7 @@ import {
|
|||||||
CompilerOptions,
|
CompilerOptions,
|
||||||
ContractArtifact,
|
ContractArtifact,
|
||||||
ContractDirectory,
|
ContractDirectory,
|
||||||
ContractIds,
|
ContractIdToSourceFileId,
|
||||||
ContractNetworkData,
|
ContractNetworkData,
|
||||||
ContractNetworks,
|
ContractNetworks,
|
||||||
ContractSourceDataByFileId,
|
ContractSourceDataByFileId,
|
||||||
@ -75,6 +75,9 @@ export class Compiler {
|
|||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
};
|
};
|
||||||
const source = await fsWrapper.readFileAsync(contentPath, opts);
|
const source = await fsWrapper.readFileAsync(contentPath, opts);
|
||||||
|
if (!_.startsWith(contentPath, contractBaseDir)) {
|
||||||
|
throw new Error(`Expected content path '${contentPath}' to begin with '${contractBaseDir}'`);
|
||||||
|
}
|
||||||
const sourceFilePath = contentPath.slice(contractBaseDir.length);
|
const sourceFilePath = contentPath.slice(contractBaseDir.length);
|
||||||
sources[sourceFilePath] = source;
|
sources[sourceFilePath] = source;
|
||||||
logUtils.log(`Reading ${sourceFilePath} source...`);
|
logUtils.log(`Reading ${sourceFilePath} source...`);
|
||||||
@ -114,7 +117,7 @@ export class Compiler {
|
|||||||
await createDirIfDoesNotExistAsync(this._artifactsDir);
|
await createDirIfDoesNotExistAsync(this._artifactsDir);
|
||||||
await createDirIfDoesNotExistAsync(SOLC_BIN_DIR);
|
await createDirIfDoesNotExistAsync(SOLC_BIN_DIR);
|
||||||
this._contractSources = {};
|
this._contractSources = {};
|
||||||
const contractIds: ContractIds = {};
|
const contractIdToSourceFileId: ContractIdToSourceFileId = {};
|
||||||
const contractDirs = Array.from(this._contractDirs.values());
|
const contractDirs = Array.from(this._contractDirs.values());
|
||||||
for (const contractDir of contractDirs) {
|
for (const contractDir of contractDirs) {
|
||||||
const sources = await Compiler._getContractSourcesAsync(contractDir.path, contractDir.path);
|
const sources = await Compiler._getContractSourcesAsync(contractDir.path, contractDir.path);
|
||||||
@ -127,18 +130,20 @@ export class Compiler {
|
|||||||
this._contractSources[sourceFileId] = source;
|
this._contractSources[sourceFileId] = source;
|
||||||
// Create a mapping between the contract id and its source file id
|
// Create a mapping between the contract id and its source file id
|
||||||
const contractId = constructContractId(contractDir.namespace, sourceFilePath);
|
const contractId = constructContractId(contractDir.namespace, sourceFilePath);
|
||||||
if (!_.isUndefined(contractIds[contractId])) {
|
if (!_.isUndefined(contractIdToSourceFileId[contractId])) {
|
||||||
throw new Error(`Found duplicate contract with ID '${contractId}'`);
|
throw new Error(`Found duplicate contract with ID '${contractId}'`);
|
||||||
}
|
}
|
||||||
contractIds[contractId] = sourceFileId;
|
contractIdToSourceFileId[contractId] = sourceFileId;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
_.forIn(this._contractSources, this._setContractSpecificSourceData.bind(this));
|
_.forIn(this._contractSources, this._setContractSpecificSourceData.bind(this));
|
||||||
const specifiedContractIds = this._specifiedContracts.has(ALL_CONTRACTS_IDENTIFIER)
|
const specifiedContractIds = this._specifiedContracts.has(ALL_CONTRACTS_IDENTIFIER)
|
||||||
? _.keys(contractIds)
|
? _.keys(contractIdToSourceFileId)
|
||||||
: Array.from(this._specifiedContracts.values());
|
: Array.from(this._specifiedContracts.values());
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
_.map(specifiedContractIds, async contractId => this._compileContractAsync(contractIds[contractId])),
|
_.map(specifiedContractIds, async contractId =>
|
||||||
|
this._compileContractAsync(contractIdToSourceFileId[contractId]),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
|
@ -83,7 +83,7 @@ export interface ContractSources {
|
|||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ContractIds {
|
export interface ContractIdToSourceFileId {
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,12 +36,12 @@ describe('Metacoin', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('#transfer', () => {
|
describe('#transfer', () => {
|
||||||
it(`should successfully transfer tokens (via transfer_1)`, async () => {
|
it(`should successfully transfer tokens (via transfer1)`, async () => {
|
||||||
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
|
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
|
||||||
const amount = INITIAL_BALANCE.div(2);
|
const amount = INITIAL_BALANCE.div(2);
|
||||||
const oldBalance = await metacoin.balances.callAsync(ZERO_ADDRESS);
|
const oldBalance = await metacoin.balances.callAsync(ZERO_ADDRESS);
|
||||||
expect(oldBalance).to.be.bignumber.equal(0);
|
expect(oldBalance).to.be.bignumber.equal(0);
|
||||||
const txHash = await metacoin.transfer_1.sendTransactionAsync(
|
const txHash = await metacoin.transfer1.sendTransactionAsync(
|
||||||
{
|
{
|
||||||
to: ZERO_ADDRESS,
|
to: ZERO_ADDRESS,
|
||||||
amount,
|
amount,
|
||||||
@ -59,13 +59,13 @@ describe('Metacoin', () => {
|
|||||||
expect(newBalance).to.be.bignumber.equal(amount);
|
expect(newBalance).to.be.bignumber.equal(amount);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`should successfully transfer tokens (via transfer_2)`, async () => {
|
it(`should successfully transfer tokens (via transfer2)`, async () => {
|
||||||
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
|
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
|
||||||
const amount = INITIAL_BALANCE.div(2);
|
const amount = INITIAL_BALANCE.div(2);
|
||||||
const oldBalance = await metacoin.balances.callAsync(ZERO_ADDRESS);
|
const oldBalance = await metacoin.balances.callAsync(ZERO_ADDRESS);
|
||||||
expect(oldBalance).to.be.bignumber.equal(0);
|
expect(oldBalance).to.be.bignumber.equal(0);
|
||||||
const callback = 59;
|
const callback = 59;
|
||||||
const txHash = await metacoin.transfer_2.sendTransactionAsync(
|
const txHash = await metacoin.transfer2.sendTransactionAsync(
|
||||||
{
|
{
|
||||||
to: ZERO_ADDRESS,
|
to: ZERO_ADDRESS,
|
||||||
amount,
|
amount,
|
||||||
@ -84,13 +84,13 @@ describe('Metacoin', () => {
|
|||||||
expect(newBalance).to.be.bignumber.equal(amount);
|
expect(newBalance).to.be.bignumber.equal(amount);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`should successfully transfer tokens (via transfer_3)`, async () => {
|
it(`should successfully transfer tokens (via transfer3)`, async () => {
|
||||||
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
|
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
|
||||||
const amount = INITIAL_BALANCE.div(2);
|
const amount = INITIAL_BALANCE.div(2);
|
||||||
const oldBalance = await metacoin.balances.callAsync(ZERO_ADDRESS);
|
const oldBalance = await metacoin.balances.callAsync(ZERO_ADDRESS);
|
||||||
expect(oldBalance).to.be.bignumber.equal(0);
|
expect(oldBalance).to.be.bignumber.equal(0);
|
||||||
const callback = 59;
|
const callback = 59;
|
||||||
const txHash = await metacoin.transfer_3.sendTransactionAsync(
|
const txHash = await metacoin.transfer3.sendTransactionAsync(
|
||||||
{
|
{
|
||||||
transferData: {
|
transferData: {
|
||||||
to: ZERO_ADDRESS,
|
to: ZERO_ADDRESS,
|
||||||
|
@ -12,63 +12,60 @@ export const abiUtils = {
|
|||||||
}
|
}
|
||||||
return param.type;
|
return param.type;
|
||||||
},
|
},
|
||||||
getFunctionSignature(abi: MethodAbi): string {
|
getFunctionSignature(methodAbi: MethodAbi): string {
|
||||||
const functionName = abi.name;
|
const functionName = methodAbi.name;
|
||||||
const parameterTypeList = abi.inputs.map((param: DataItem) => this.parseFunctionParam(param));
|
const parameterTypeList = _.map(methodAbi.inputs, (param: DataItem) => this.parseFunctionParam(param));
|
||||||
const functionSignature = `${functionName}(${parameterTypeList})`;
|
const functionSignature = `${functionName}(${parameterTypeList})`;
|
||||||
return functionSignature;
|
return functionSignature;
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Solidity supports function overloading whereas TypeScript does not.
|
||||||
|
* See: https://solidity.readthedocs.io/en/v0.4.21/contracts.html?highlight=overload#function-overloading
|
||||||
|
* In order to support overloaded functions, we suffix overloaded function names with an index.
|
||||||
|
* This index should be deterministic, regardless of function ordering within the smart contract. To do so,
|
||||||
|
* we assign indexes based on the alphabetical order of function signatures.
|
||||||
|
*
|
||||||
|
* E.g
|
||||||
|
* ['f(uint)', 'f(uint,byte32)']
|
||||||
|
* Should always be renamed to:
|
||||||
|
* ['f1(uint)', 'f2(uint,byte32)']
|
||||||
|
* Regardless of the order in which these these overloaded functions are declared within the contract ABI.
|
||||||
|
*/
|
||||||
renameOverloadedMethods(inputContractAbi: ContractAbi): ContractAbi {
|
renameOverloadedMethods(inputContractAbi: ContractAbi): ContractAbi {
|
||||||
const contractAbi = _.cloneDeep(inputContractAbi);
|
const contractAbi = _.cloneDeep(inputContractAbi);
|
||||||
const methodAbis = contractAbi.filter((abi: AbiDefinition) => abi.type === AbiType.Function) as MethodAbi[];
|
const methodAbis = contractAbi.filter((abi: AbiDefinition) => abi.type === AbiType.Function) as MethodAbi[];
|
||||||
const methodAbisByOriginalIndex = _.transform(
|
|
||||||
methodAbis,
|
|
||||||
(result: Array<{ index: number; methodAbi: MethodAbi }>, methodAbi, i: number) => {
|
|
||||||
result.push({ index: i, methodAbi });
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
// Sort method Abis into alphabetical order, by function signature
|
// Sort method Abis into alphabetical order, by function signature
|
||||||
const methodAbisByOriginalIndexOrdered = _.sortBy(methodAbisByOriginalIndex, [
|
const methodAbisOrdered = _.sortBy(methodAbis, [
|
||||||
(entry: { index: number; methodAbi: MethodAbi }) => {
|
(methodAbi: MethodAbi) => {
|
||||||
const functionSignature = this.getFunctionSignature(entry.methodAbi);
|
const functionSignature = this.getFunctionSignature(methodAbi);
|
||||||
return functionSignature;
|
return functionSignature;
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
// Group method Abis by name (overloaded methods will be grouped together, in alphabetical order)
|
// Group method Abis by name (overloaded methods will be grouped together, in alphabetical order)
|
||||||
const methodAbisByName = _.transform(
|
const methodAbisByName: { [key: string]: MethodAbi[] } = {};
|
||||||
methodAbisByOriginalIndexOrdered,
|
_.each(methodAbisOrdered, methodAbi => {
|
||||||
(result: { [key: string]: Array<{ index: number; methodAbi: MethodAbi }> }, entry) => {
|
(methodAbisByName[methodAbi.name] || (methodAbisByName[methodAbi.name] = [])).push(methodAbi);
|
||||||
(result[entry.methodAbi.name] || (result[entry.methodAbi.name] = [])).push(entry);
|
});
|
||||||
},
|
// Rename overloaded methods to overloadedMethodName1, overloadedMethodName2, ...
|
||||||
{},
|
_.each(methodAbisByName, methodAbisWithSameName => {
|
||||||
);
|
_.each(methodAbisWithSameName, (methodAbi, i: number) => {
|
||||||
// Rename overloaded methods to overloadedMethoName_1, overloadedMethoName_2, ...
|
|
||||||
const methodAbisRenamed = _.transform(
|
|
||||||
methodAbisByName,
|
|
||||||
(result: MethodAbi[], methodAbisWithSameName: Array<{ index: number; methodAbi: MethodAbi }>) => {
|
|
||||||
_.forEach(methodAbisWithSameName, (entry, i: number) => {
|
|
||||||
if (methodAbisWithSameName.length > 1) {
|
if (methodAbisWithSameName.length > 1) {
|
||||||
const overloadedMethodId = i + 1;
|
const overloadedMethodId = i + 1;
|
||||||
const sanitizedMethodName = `${entry.methodAbi.name}_${overloadedMethodId}`;
|
const sanitizedMethodName = `${methodAbi.name}${overloadedMethodId}`;
|
||||||
const indexOfExistingAbiWithSanitizedMethodNameIfExists = _.findIndex(
|
const indexOfExistingAbiWithSanitizedMethodNameIfExists = _.findIndex(
|
||||||
methodAbis,
|
methodAbis,
|
||||||
methodAbi => methodAbi.name === sanitizedMethodName,
|
currentMethodAbi => currentMethodAbi.name === sanitizedMethodName,
|
||||||
);
|
);
|
||||||
if (indexOfExistingAbiWithSanitizedMethodNameIfExists >= 0) {
|
if (indexOfExistingAbiWithSanitizedMethodNameIfExists >= 0) {
|
||||||
const methodName = entry.methodAbi.name;
|
const methodName = methodAbi.name;
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to rename overloaded method '${methodName}' to '${sanitizedMethodName}'. A method with this name already exists.`,
|
`Failed to rename overloaded method '${methodName}' to '${sanitizedMethodName}'. A method with this name already exists.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
entry.methodAbi.name = sanitizedMethodName;
|
methodAbi.name = sanitizedMethodName;
|
||||||
}
|
}
|
||||||
// Add method to list of ABIs in its original position
|
|
||||||
result.splice(entry.index, 0, entry.methodAbi);
|
|
||||||
});
|
});
|
||||||
},
|
});
|
||||||
[...Array(methodAbis.length)],
|
|
||||||
);
|
|
||||||
return contractAbi;
|
return contractAbi;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -354,13 +354,7 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
redux "*"
|
redux "*"
|
||||||
|
|
||||||
"@types/request-promise-native@^1.0.2":
|
"@types/request@2.47.0":
|
||||||
version "1.0.14"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/request-promise-native/-/request-promise-native-1.0.14.tgz#20f2ba136e6f29c2ea745c60767738d434793d31"
|
|
||||||
dependencies:
|
|
||||||
"@types/request" "*"
|
|
||||||
|
|
||||||
"@types/request@*", "@types/request@2.47.0":
|
|
||||||
version "2.47.0"
|
version "2.47.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/request/-/request-2.47.0.tgz#76a666cee4cb85dcffea6cd4645227926d9e114e"
|
resolved "https://registry.yarnpkg.com/@types/request/-/request-2.47.0.tgz#76a666cee4cb85dcffea6cd4645227926d9e114e"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user