Hello,
I am working on a simple EIP712 whitelist member wallet registration/validation scheme. The nutshell goes (sign typed data -> pass to chain -> extract signing address -> compare to signing address stored on chain).
I have been banging my head against this for a while now. I am not able to get the onchain extracted address to match the signing public address offchain. My eyes are way too close to this problem and I need help looking for something I may have missed. By my best ability, I appear to be adhering to standard, but obviously I am doing something wrong.
I have been referring to the EIP712 standard, the 'Mail' EIP reference implementation here (sol) + here (js), and the msfeldstein reference implementation here (sol) + here (ts).
Constraint
For reasons, I do not wish to use any framework/OpenZeppelin (and I also have tried, but likewise could not get to work.)
Notes
The code presented below is basically the EIP reference implementation whittled down, and made as painfully explicit as possible to make the troubleshooting/review process as easy as possible. I likewise cut out all the other testing console.logs.
My approach has been to generate the v, r, s, and signing public address by running .js and printing to console. I then deploy the .sol to Remix, and manually enter generated values.
I am likewise posting the question on Ethereum Stack Exchange, etc.
Alternative typed-data signing methods/strategies are verymuch welcome.
If you have the time and knowhow, I would appreciate your review of my implementation of the EIP712 standard below.
Clientside:
// using ethereumjs-util 7.1.3
const ethUtil = require('ethereumjs-util');
// using ethereumjs-abi 0.6.9
const abi = require('ethereumjs-abi');
// The purpose of this script is to be painfully explicit for the sake
// of showing work, to ask for help.
// generate keys
prikey = ethUtil.keccakFromString('cow', 256);
signingAddress = ethUtil.privateToAddress(prikey);
// 0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826
// data
const typedData = {
types: {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
],
Validation: [
{ name: 'wallet', type: 'address' },
{ name: 'share', type: 'uint256' },
{ name: 'pool', type: 'uint8' }
],
},
primaryType: 'Validation',
domain: {
name: 'Validator',
version: '1',
chainId: 1,
verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
},
message: {
wallet: '0xeeBA65D9C7E5832918d1F4277DE0a78b78efEC43',
share: 1000,
pool: 5,
},
};
// create domain struct hash
const encodedDomainType = 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)';
const domainTypeHash = ethUtil.keccakFromString(encodedDomainType, 256);
var encTypes = [];
var encValues = [];
// add typehash
encTypes.push('bytes32');
encValues.push(domainTypeHash);
// add name
encTypes.push('bytes32');
encValues.push(ethUtil.keccakFromString(typedData.domain.name, 256));
// add version
encTypes.push('bytes32');
encValues.push(ethUtil.keccakFromString(typedData.domain.version, 256));
// add chainId
encTypes.push('uint256');
encValues.push(typedData.domain.chainId);
// add chainId
encTypes.push('address');
encValues.push(typedData.domain.verifyingContract);
// computer final hash
domainStructHash = abi.rawEncode(encTypes, encValues);
// create validation struct hash
const encodedValidationType = 'Validation(address wallet,uint256 share,uint256 pool)';
const validationTypeHash = ethUtil.keccakFromString(encodedValidationType, 256);
encTypes = [];
encValues = [];
// add typehash
encTypes.push('bytes32');
encValues.push(validationTypeHash);
// add wallet address
encTypes.push('address');
encValues.push(typedData.message.wallet);
// add share
encTypes.push('uint256');
encValues.push(typedData.message.share);
// add pool
encTypes.push('uint256');
encValues.push(typedData.message.pool);
// computer final hash
validationStructHash = abi.rawEncode(encTypes, encValues);
// now finally create final signature hash
signatureHash = ethUtil.keccak256(
Buffer.concat([
Buffer.from('1901', 'hex'),
domainStructHash,
validationStructHash,
]),
);
// and finally, sign
signature = ethUtil.ecsign(signatureHash, prikey);
// convert r, s, and signingAddress into hex strings to pass to remix
console.log(signature.v);
var r = ''
function pad2(s) {return s.length < 2 ? "0" + s : s};
for(i = 0; i < signature.r.length; i++) {
r += pad2(signature.r[i].toString(16)); }
console.log('0x' + r); // r bytes
var s = ''
function pad2(s) {return s.length < 2 ? "0" + s : s};
for(i = 0; i < signature.s.length; i++) {
s += pad2(signature.s[i].toString(16)); }
console.log('0x' + s); // s bytes
var str = '';
function pad2(s) {return s.length < 2 ? "0" + s : s};
for(i = 0; i < signingAddress.length; i++) {
str += pad2(signingAddress[i].toString(16)); }
console.log('0x' + str); // signingAddress bytes
On chain:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
contract validateData {
address _validationKey = 0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826;
struct EIP712Domain {
string name;
string version;
uint256 chainId;
address verifyingContract;
}
struct Validation {
address wallet;
uint256 share;
uint256 pool;
}
bytes32 constant EIP712DOMAIN_TYPEHASH = keccak256(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
);
bytes32 constant VALIDATION_TYPEHASH = keccak256(
"Validation(address wallet,uint256 share,uint256 pool)"
);
bytes32 DOMAIN_SEPARATOR;
constructor () {
DOMAIN_SEPARATOR = hash(EIP712Domain({
name: "Validator",
version: '1',
chainId: 1,
verifyingContract: 0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC
}));
}
function hash(EIP712Domain memory eip712Domain) internal pure returns (bytes32) {
return keccak256(abi.encode(
EIP712DOMAIN_TYPEHASH,
keccak256(bytes(eip712Domain.name)),
keccak256(bytes(eip712Domain.version)),
eip712Domain.chainId,
eip712Domain.verifyingContract
));
}
function hash(Validation calldata validation) internal pure returns (bytes32) {
return keccak256(abi.encode(
VALIDATION_TYPEHASH,
validation.wallet,
validation.share,
validation.pool
));
}
event compare(address sig, address key);
function verify(Validation calldata validation, uint8 v, bytes32 r, bytes32 s) public {
bytes32 digest = keccak256(abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
hash(validation)
));
emit compare(ecrecover(digest, v, r, s), _validationKey);
}
}
Thank you for your time and consideration!
Use this contract template to recover your address first, then use it into your contract, when you successfully retrieve the address. I will explain the contract too:
pragma solidity ^0.8.0;
contract SignTest {
address owner = msg.sender;
mapping(uint256 => bool) usedNonces;
function test(uint256 amount, uint256 nonce, bytes memory sig, uint tV, bytes32 tR, bytes32 tS, bytes32 tMsg) public view returns(address) {
bytes32 message = prefixed(keccak256(abi.encodePacked(amount, nonce)));
bytes32 messageWithoutPrefix = keccak256(abi.encodePacked(amount, nonce));
address signer = recoverSigner(messageWithoutPrefix, sig, tV, tR,tS);
return signer;
}
// Signature methods
function splitSignature(bytes memory sig)
public
view
returns (uint8, bytes32, bytes32)
{
require(sig.length == 65, "B");
bytes32 r;
bytes32 s;
uint8 v;
assembly {
// first 32 bytes, after the length prefix
r := mload(add(sig, 32))
// second 32 bytes
s := mload(add(sig, 64))
// final byte (first byte of the next 32 bytes)
v := byte(0, mload(add(sig, 96)))
}
return (v, r, s);
}
function recoverSigner(bytes32 message, bytes memory sig, uint tV, bytes32 tR, bytes32 tS)
public
view
returns (address)
{
uint8 v;
bytes32 r;
bytes32 s;
(v, r, s) = splitSignature(sig);
require(v==tV, "V is not correct");
require(r==tR, "R is not correct");
require(s==tS, "S is not correct");
return ecrecover(message, v, r, s);
}
// Builds a prefixed hash to mimic the behavior of eth_sign.
function prefixed(bytes32 inputHash) public pure returns (bytes32) {
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", inputHash));
}
}
I use web3 for ecrecover, so, let me provide you a simple example:
let fnSignature = web3.utils.keccak256("setApprovalForAll(address,bool").substr(0,10)
// encode the function parameters and add them to the call data
let fnParams = web3.eth.abi.encodeParameters(
["address","bool"],
[toAddr,permit]
)
calldata = fnSignature + fnParams.substr(2)
console.log(calldata)
First, substr first 4 bytes of function signature, then encode them into fnParams + fnSignature
Second step is to sign your data:
const data = calldata //Retrieved from above code
const NFTAddress = 'Contract address where you sign'
const newSigner = web3.eth.accounts.privateKeyToAccount("Your Priv Key");
const myAccount = web3.eth.accounts.wallet.add(newSigner);
const signer = myAccount.address;
console.log(signer) // display the target address in console ( for better verification )
Use calldata from first function and add it to data
Add the contract address where you want to sign the hash
Then, follow these steps:
let rawData = web3.eth.abi.encodeParameters(
['address','bytes'],
[NFTAddress,data]
);
// hash the data.
let hash = web3.utils.soliditySha3(rawData);
console.log(hash)
// sign the hash.
let signature = web3.eth.sign(hash, signer);
console.log(signature)
Using "hash", go to solidity contract (posted by me) and add retrieved bytes(hash) into function prefixed(bytes32 hash)
Using "signature", go to solidity contract and retrieve v,r,s. Use function splitSignature(bytes32 signature)
Using bytes generated by function prefixed(bytes32 hash), use function recoverSigner(message ( bytes from prefixed(bytes32hash), signature ( from web3 javascript), v ( uint8 from splitSignature), r (bytes32 from splitSignature), s ( bytes32 from splitSignature)
You can also play with the contract and script I provided, but, i prefer to post a step by step guide, easy to understand for everyone.
Happy developing! :)
Related
My smart contract has a struct and a function that populates it:
struct voter {
uint ID;
string firstName;
string lastName;
uint phone;
string addy;
//add picture
}
contract Poll {
uint public numVoters;
event VoterAdded(
voter newVoter
);
function AddVoter(string memory _firstName, string memory _lastName, uint _phone, string
memory _addy) public returns (voter memory){
numVoters++;
voter memory _voter = voter(numVoters, _firstName, _lastName, _phone, _addy);
_voter.ID = numVoters;
_voter.firstName = _firstName;
_voter.lastName = _lastName;
_voter.phone = _phone;
_voter.addy = _addy;
emit VoterAdded(_voter);
return _voter;
}
}
I am using truffle to test this struct, and Im trying to populate a struct and then storing a struct variable in a javascript variable.
const Poll = artifacts.require('Poll.sol');
it('Poll 1 : create voter and candidate objects2', async () =>
const tx = await poll.AddVoter('Jack', 'Jackson', 0, '');
const results = await poll.getPastEvents(
'VoterAdded',
{
fromBlock: 0, toBlock: 'latest'
});
console.log("Results", results, '${results.length} results');
const JJ = results[0];
assert.equal(JJ.firstName, 'Jack');
});
I think there is a problem with the lines after getPastEvents().
Heres the error I get:
Poll 1 : create voter and candidate objects2:
AssertionError: expected undefined to equal 'Jack'
I don't know if it's going to be of use or not, but I always used queryFilter to get the past events, using etherjs.
const eventFilter = poll.filters.VoterAdded();
const events = await poll.queryFilter(eventFilter);
console.log(events);
My knowledge of javascript is limited. However, with much reading I was able to hack together the functions below. The goal is to define a generic function with which I can extract certain data from Yahoo finance.
In the example below, I'm pulling in A LOT of JSON data for any ticker (example link at the bottom). From the returned JSON, I select which values I'm interested in (here only dividendRate and paypoutRatio. If you call this function in a cell (I use Google Sheets), it will return 2 cells with the requested information:
=yFinance("KO")
+------+
| 1.76 |
+------+
| 0.75 |
+------+
Link used: Yahoo Finance JSON for Coca Cola
What I need, however, is to have the data formatted not in 2 cells BELOW each other, but NEXT TO each other:
=yFinance("KO")
+------+------+
| 1.76 | 0.75 |
+------+------+
I have two questions:
How can the code (probably in getMatchingValues()?) be updated to make this happen?
Is there any way to simplify these functions?
Any help is greatly appreciated, also because I feel that a generic function like this would benefit many people in the future. I have seen many posts about extracting a single value from Yahoo Finance, mostly by parsing the HTML (tables) using Google Sheets INDEX() function, but I believe via the JSON is faster and more resistent to future front-end changes at Yahoo.
function yFinance(symbol) {
const url = 'https://query2.finance.yahoo.com/v10/finance/quoteSummary/' + encodeURI(symbol)
+ '?modules=price,assetProfile,summaryDetail,incomeStatementHistory,'
+ 'balanceSheetHistory,defaultKeyStatistics,financialData,calendarEvents,'
+ 'recommendationTrend,upgradeDowngradeHistory,majorHoldersBreakdown'
;
const response = UrlFetchApp.fetch(url, {muteHttpExceptions: true});
const responseCode = response.getResponseCode();
if (responseCode === 200) {
const quote = JSON.parse(response.getContentText());
const paths = [
'summaryDetail/dividendRate/raw',
'summaryDetail/payoutRatio/raw'
];
return getMatchingValues(getPath(quote, 'quoteSummary/result/0'), paths);
}
else
{
return -1;
}
}
function getPath( obj, path ) {
if (!path || !obj) {
return null;
}
const parts = path.split('/');
const currentPath = parts[0];
const nextPath = parts.slice(1).join('/');
const currentObj = obj[currentPath];
// final path, exit here
if (nextPath.length === 0) {
return currentObj;
}
if (Array.isArray( currentObj )) {
// does an element exit at the next level
if ( currentObj[parts[1]] ) {
// will continue reading for 1 element
return getPath( currentObj, nextPath );
}
// return all the subpaths, skip the ones that are falsy
return currentObj.map( item => getPath( item, nextPath ) ).filter( v => v );
}
// get the next part of the object
return getPath( currentObj, nextPath );
}
function getMatchingValues( obj, paths ) {
return paths.flatMap( path => getPath( obj, path ));
}
If you want to get side by side values, modify by adding brackets as follows
return [getMatchingValues(getPath(quote, 'quoteSummary/result/0'), paths)];
You can simplify the code as follows
function yFinance_new(symbol) {
const url = 'https://query2.finance.yahoo.com/v10/finance/quoteSummary/' + encodeURI(symbol)
+ '?modules=price,assetProfile,summaryDetail,incomeStatementHistory,'
+ 'balanceSheetHistory,defaultKeyStatistics,financialData,calendarEvents,'
+ 'recommendationTrend,upgradeDowngradeHistory,majorHoldersBreakdown'
;
const response = UrlFetchApp.fetch(url, { muteHttpExceptions: true });
const responseCode = response.getResponseCode();
if (responseCode === 200) {
const quote = JSON.parse(response.getContentText());
return [[
quote.quoteSummary.result[0].summaryDetail.dividendRate.raw,
quote.quoteSummary.result[0].summaryDetail.payoutRatio.raw
]];
}
else {
return -1;
}
}
Have a simple contract in solidity:
contract SellStuff{
address seller;
string name;
string description;
uint256 price;
function sellStuff(string memory _name, string memory _description, uint256 _price) public{
seller = msg.sender;
name = _name;
description = _description;
price = _price;
}
function getStuff() public view returns (
address _seller,
string memory _name,
string memory _description,
uint256 _price){
return(seller, name, description, price);
}
}
And running a javascript test as follows:
var SellStuff= artifacts.require("./SellStuff.sol");
// Testing
contract('SellStuff', function(accounts){
var sellStuffInstance;
var seller = accounts[1];
var stuffName = "stuff 1";
var stuffDescription = "Description for stuff 1";
var stuffPrice = 10;
it("should sell stuff", function(){
return SellStuff.deployed().then(function(instance){
sellStuffInstance= instance;
return sellStuffInstance.sellStuff(stuffName, stuffDescription, web3.utils.toWei(stuffPrice,'ether'), {from: seller});
}).then(function(){
//the state of the block should be updated from the last promise
return sellStuffInstance.getStuff();
}).then(function(data){
assert.equal(data[0], seller, "seller must be " + seller);
assert.equal(data[1], stuffName, "stuff name must be " + stuffName);
assert.equal(data[2], stuffDescription, "stuff description must be " + stuffDescription);
assert.equal(data[3].toNumber(), web3.utils.toWei(stuffPrice,"ether"), "stuff price must be " + web3.utils.toWei(stuffPrice,"ether"));
});
});
});
But I am getting the following error:
Error: Please pass numbers as string or BN objects to avoid precision errors.
This seems to look like it pertains to the return type from the web3.utils.toWei call, so I have tried to cast it to a string:web3.utils.toWei(stuffPrice.toString(),"ether"); but this gives the Error: Number can only safely store up to 53 bits.
Not sure if I need to simply change the var in the class from uint256 or if there is a better way to cast the toWei return variable?
The error is here:
web3.utils.toWei(stuffPrice,'ether')
stuffPrice should be string.
web3.utils.toWei(String(stuffPrice),'ether')
The toWei() method accepts String|BN as the first argument. You're passing it the stuffPrice as a Number.
A quick fix is to define the stuffPrice as String:
var stuffPrice = '10'; // corrected code, String
instead of
var stuffPrice = 10; // original code, Number
Another way is to pass it a BN object.
var stuffPrice = 10; // original code, Number
web3.utils.toWei(
web3.utils.toBN(stuffPrice), // converts Number to BN, which is accepted by `toWei()`
'ether'
);
You need to declare the state variable as a string which will work in this case.
In React:
state = { playerEthervalue: ''};
const accounts = await web3.eth.getAccounts();
// Send the ethers to transaction, initiate the transaction
await lottery.methods.getPlayersAddress().send({ from: accounts[0],
value: web3.utils.toWei(this.state.playerEthervalue, 'ether') });
In solidity(.sol):
function getPlayersAddress() public payable {
require(msg.value >= 0.00000001 ether);
players.push(msg.sender);
}
I am writing a unit test for my smart contract using truffle and when I run the test using "truffle test" the test failed with this error "Error: Returned error: VM Exception while processing transaction: invalid opcode".
smart contract code:
pragma solidity ^0.4.3;
contract owned {
address public owner;
/* Initialise contract creator as owner */
function owned() public {
owner = msg.sender;
}
/* Function to dictate that only the designated owner can call a function */
modifier onlyOwner {
require(owner == msg.sender);
_;
}
/* Transfer ownership of this contract to someone else */
function transferOwnership(address newOwner) public onlyOwner() {
owner = newOwner;
}
}
/*
* #title AsnScRegistry
* Open Vote Network
* A self-talling protocol that supports voter privacy.
*
* Author: Shahrul Sharudin
*/
contract AsnScRegistry is owned{
struct IPAddress {
uint128 ip;
uint8 mask;
}
struct MemberAddresses {
address contractAddress;
address walletAddress;
uint index;
}
mapping (uint => IPAddress[]) managedIps;
mapping (uint => MemberAddresses) ethAddresses;
uint[] private registeredAsn;
function getManagedIpByAsn(uint _asn) view public returns (uint128[] _ip, uint8[] _mask){
IPAddress[] memory addresses = managedIps[_asn];
uint128[] memory ips = new uint128[](addresses.length);
uint8[] memory mask = new uint8[](addresses.length);
for (uint i = 0; i < addresses.length; i++) {
ips[i] = addresses[i].ip;
mask[i] = addresses[i].mask;
}
return (ips, mask);
}
function getMemberAddressesByAsn(uint _asn) view public returns (address _contractAddress, address _walletAddress){
MemberAddresses memory memberAddresses = ethAddresses[_asn];
return (memberAddresses.contractAddress, memberAddresses.walletAddress);
}
function addMember(uint _asn, uint128[] _ip, uint8[] _mask, address _contractAddress, address _walletAddress) public {
MemberAddresses memory member = ethAddresses[_asn];
//throw error if member already exist
assert(member.contractAddress != 0);
for(uint i=0; i<_ip.length; i++){
managedIps[_asn].push(IPAddress({ip:_ip[i], mask:_mask[i]}));
}
uint idx = registeredAsn.push(_asn)-1;
ethAddresses[_asn] = MemberAddresses({contractAddress:_contractAddress, walletAddress:_walletAddress, index:idx});
}
function removeMember(uint _asn) public returns (uint[] _registeredAsn){
//uint memory index = ethAddresses[_asn].index;
if (ethAddresses[_asn].index >= registeredAsn.length) return;
for (uint i = ethAddresses[_asn].index; i<registeredAsn.length - 1; i++){
registeredAsn[i] = registeredAsn[i+1];
}
registeredAsn.length--;
delete managedIps[_asn];
delete ethAddresses[_asn];
return registeredAsn;
}
function getTotalMembers() view public returns (uint _totalMembers) {
return (registeredAsn.length);
}
function getRegisteredAsn() view public returns (uint[] _asn){
return (registeredAsn);
}
}
javascript unit test:
const AsnScRegistry = artifacts.require('./AsnScRegistry.sol')
const assert = require('assert')
const ip = [3524503734,3232235776];
const mask = [255,255];
const contractAddress = '0x1234567890123456789012345678901234567891';
const walletAddress = '0x1234567890123456789012345678901234567891';
const asn = 123;
let registryInstance;
contract("AsnScRegistry", accounts => {
it("Should add a new member", () =>
AsnScRegistry.deployed()
.then(instance => {
registryInstance = instance;
return instance.addMember(asn, ip, mask, contractAddress, walletAddress);
})
.then(() => registryInstance.getTotalMembers())
.then(total => assert.equal(total.toNumber(), 1, "xxxxx"))
);
});
the full source is available here: https://github.com/shaza4061/electioncommissioner
I expect the addMember() function to store the information in the smart contract and getTotalMembers() to return the number of record in registeredAsn[] array and the assertion in the unit test to pass.
The following error message:
"Error: Returned error: VM Exception while processing transaction: invalid opcode"
indicates a failed assert statement in solidity.
In your code, you have an assert which causes this error.
//throw error if member already exist
assert(member.contractAddress != 0);
Here is a manual for Solidity Smart contract debugging using Truffle.
I've the below JavaScript code, that returned a function callback related to the user command, the user command could be used in different ways, hence RegEx is required:
(function (undefined) {
"use strict";
var root = this;
var commandsList = [];
var debugStyle = 'font-weight: bold; color: #00f;';
// The command matching code is a modified version of Backbone.Router by Jeremy Ashkenas, under the MIT license.
var optionalParam = /\s*\((.*?)\)\s*/g;
var optionalRegex = /(\(\?:[^)]+\))\?/g;
var namedParam = /(\(\?)?:\w+/g;
var splatParam = /\*\w+/g;
var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#]/g;
var commandToRegExp = function(command) {
command = command.replace(escapeRegExp, '\\$&')
.replace(optionalParam, '(?:$1)?')
.replace(namedParam, function(match, optional) {
return optional ? match : '([^\\s]+)';
})
.replace(splatParam, '(.*?)')
.replace(optionalRegex, '\\s*$1?\\s*');
return new RegExp('^' + command + '$', 'i');
};
var registerCommand = function(command, cb, phrase) {
commandsList.push({ command: command, callback: cb, originalPhrase: phrase });
root.console.log('Command successfully loaded: %c'+phrase, debugStyle);
};
root.fonixListen = {
addCommands: function(commands) {
var cb;
for (var phrase in commands) {
if (commands.hasOwnProperty(phrase)) {
cb = root[commands[phrase]] || commands[phrase];
if (typeof cb === 'function') {
// convert command to regex then register the command
registerCommand(commandToRegExp(phrase), cb, phrase);
} else if (typeof cb === 'object' && cb.regexp instanceof RegExp) {
// register the command
registerCommand(new RegExp(cb.regexp.source, 'i'), cb.callback, phrase);
}
}
}
},
executeCommand: function(commandText) {
for (var j = 0, l = commandsList.length; j < l; j++) {
var result = commandsList[j].command.exec(commandText);
if (result) {
var parameters = result.slice(1);
// execute the matched command
commandsList[j].callback.apply(this, parameters);
return true;
}
}
}
};
}).call(this)
Below are some commands:
var commands = {
'hello :name *term': function(name) {
alert('hello '+name+''); // i.e. consider *term as optional input
},
'items identification': {
'regexp': /^(What is|What's|Could you please tell me|Could you please give me) the meaning of (TF|FFS|SF|SHF|FF|Tube Film|Shrink Film|Stretch Hood|Stretch Hood Film|Flat Film)$/,
'callback': itemsIdentification,
},
'ML SoH': {
'regexp': /^(What is|What's|Could you please tell me|Could you please give me) the (stock|inventory) of ML$/,
'callback': mlSOH,
},
'Report stock on hand': {
'regexp': /^(What is|What's) (our|the) (stock|inventory|SoH) of (TF|FFS|SF|SHF|FF|Tube Film|Shrink Film|Stretch Hood|Stretch Hood Film|Flat Film)$/,
'callback': SoH,
},
'Basic Mathematical Opertions': {
// ?\s? can be used instead of space, also could use /i instead of $/,
'regexp': /^(What is|What's|Calculate|How much is) ([\w.]+) (\+|and|plus|\-|less|minus|\*|\x|by|multiplied by|\/|over|divided by) ([\w.]+)$/,
'callback': math,
},
};
At running the app, the addCommands command is executed, and based on the input command from the user, the executeCommand command is executed.
The above works very fine with me, but I'm moving to C#, and very new to it, so looking for help, at least guiding of some functionalities and tools in C# that can help me write something similar to the above.
UPDATE
More details about what I try to do, actually I've a form, where the user input his command by voice using HTL5 voice API, the API convert this voice into text, then this text i submitted to my app, where my app work start by looking into this text, trying to find the required command using the ReqEx, then execute the programmed function/callback that is mapped with this input command.
I found the solution using Dictionary at using System.Collections.Generic; and using RegEx at using System.Text.RegularExpressions; along with the need of a function called FirstOrDefault availabe at using System.Linq;
I used Action instead of function Func as the callbacks in my case are do not return anything, i.e. they are a void functions, and because no input parameters provided, I used Action-delegate, and did not use Action<string[]>.
The working code is:
using System;
using System.Collections.Generic; // for Dictionary
using System.Linq; // for FirstOrDefault
using System.Text.RegularExpressions; // for RegEx
namespace ConsoleApplication
{
public class Program
{
public static void Main(string[] args)
{
Console.WriteLine("Hello World!");
var input = "What's the meaning of Stretch Hood";
var functions = new Dictionary<Regex, Action>
{
{new Regex("/^(What is|What's|Could you please tell me|Could you please give me) the meaning of (TF|FFS|SF|SHF|FF|Tube Film|Shrink Film|Stretch Hood|Stretch Hood Film|Flat Film)$/"),
itemsIdentification},
{new Regex("/^(What is|What's|Could you please tell me|Could you please give me) the (stock|inventory) of ML$/"),
mlSOH},
{new Regex("/^(What is|What's) (our|the) (stock|inventory|SoH) of (TF|FFS|SF|SHF|FF|Tube Film|Shrink Film|Stretch Hood|Stretch Hood Film|Flat Film)$/"),
SoH},
{new Regex("/^(What is|What's|Calculate|How much is) ([\w.]+) (\+|and|plus|\-|less|minus|\*|\x|by|multiplied by|\/|over|divided by) ([\w.]+)$/"),
math},
};
functions.FirstOrDefault(f => f.Key.IsMatch(input)).Value?.Invoke(); // This will execute the first Action found wherever the input matching the RegEx, the ?. means if not null ([Null-conditional Operators][1])
// or
Action action;
action = functions.FirstOrDefault(f => f.Key.IsMatch(input)).Value;
if (action != null)
{
action.Invoke();
}
else
{
// No function with that name
}
}
public static void itemsIdentification()
{
Console.WriteLine("Fn 1");
}
public static void mlSOH()
{
Console.WriteLine("Fn 2");
}
public static void SoH()
{
}
public static void math()
{
}
}
}