Mallet, the minimum wallet, is a command line utility for deploying smart contracts and interacting with the Cardano IELE and KEVM testnets developed by IOHK. Mallet is written in Javascript and is based on the Node.js REPL, so it gives access to some handy functions as well as libraries such as Web3. Mallet can also be included as a library in your own JavaScript programs.
We have tested Mallet with Node.js version 10.16.3
. The repository contains a proper .nvmrc
file, so you can call nvm to use this particular version, provided it is already installed in your system.
$ git clone https://github.com/input-output-hk/mallet
$ cd mallet
$ cat .nvmrc
10.16.3
$ nvm use
Found '/tmp/mallet/.nvmrc' with version <10.16.3>
Now using node v10.16.3 (npm v6.14.5)
$ npm install
$ ./mallet -d . http://127.0.0.1:8546
The last step uses mallet to connect to a local Mantis client that accepts JSON-RPC calls on port 8546
.
There are more detailed instructions for installing Node.js and Mallet in two places.
First, there are pages on installing Node.js and Mallet along with a tutorial.
Second, there is a video, Getting Started with Mallet.
Once you have Node.js installed, run:
$ npm install -g --engine-strict @iohk/mallet
This will download and install all the dependencies. The --engine-strict flag makes sure you have the required version of Node installed.
Type the following to see Mallet's usage help:
$ mallet --help
Running Mallet with proper arguments will open a Node.js REPL session with the Mallet commands available.
mallet iele
Everything has to be valid JavaScript:
mallet> 1 + 1
2
mallet> const x = _
undefined
mallet> x
2
mallet> x = 3
TypeError: Assignment to constant variable.
mallet> help
[Function: help]
mallet> help()
undefined
- Mallet is a command line tool to be run on Node.js. Although it may be possible to import Mallet into a browser, this has not been tested and is not officially supported.
- Mallet takes advantage of
BigInt
arbitrary-precision integers, which were added to the V8 Javascript engine in May 2018. - To meet the requirements of an interactive shell environment, all Mallet's functions are synchronous.
- Because of an issue with the embeddable Node.js REPL, an augmented version of rlp.js has been put in the
lib
folder
Technically speaking, the ‘commands’ discussed here are actually functions and properties of a Mallet object. We refer to them as commands because that reflects how Mallet is used.
When running Mallet, these commands may be useful.
Opens the README file you are reading in your default browser:
mallet> help()
undefined
Lists the available commands:
mallet> listCommands()
[ 'currentAccount',
'getBalance',
'getNonce',
'getReceipt',
'help',
'iele.contractCall',
'iele.createContract',
'iele.simpleTransfer',
'importPrivateKey',
'lastTransaction',
'listAccounts',
'listCommands',
'newAccount',
'requestFunds',
'selectAccount',
'sendTransaction',
'web3' ]
These management commands operate on the local keystore and do not connect to a testnet.
Creates a random key pair for a new managed account. The private key is stored in the datadir that is encrypted with the provided password. The command returns the corresponding account’s address.
mallet> newAccount()
Enter password:
Repeat password:
'0x8cc7c261b5dda47755ac9629ec32bba0ab4d1d32'
Imports a known private key as a managed account. The private key is stored in the datadir that is encrypted with the provided password. This import command returns the corresponding account’s address.
mallet> importPrivateKey()
Enter private key:
Enter password:
Repeat password:
'0xb0ff13c14e071b11ab678524ae28e6eb248de96a'
Lists the addresses of all managed accounts:
mallet> listAccounts()
[ '0xfc7e805d72ca57aff872cd010a4c9c5e8e8f22f2',
'0xd7f3583b8805cfbe0979050f5a1b3587a8fee900',
'0x724e5991252860ca530542719b25b39b1b437a1c',
'0x60950641e7382a120c8825464391da3b84db2a86',
'0xb0ff13c14e071b11ab678524ae28e6eb248de96a' ]
Selects an account for commands such as sendTransaction
, getBalance
, requestFunds
:
mallet> selectAccount('0xfc7e805d72ca57aff872cd010a4c9c5e8e8f22f2')
'0xfc7e805d72ca57aff872cd010a4c9c5e8e8f22f2'
mallet> currentAccount()
'0xfc7e805d72ca57aff872cd010a4c9c5e8e8f22f2'
The following commands interact with nodes on Mantis, the Ethereum client, via the JSON remote procedure call (RPC) protocol (using the Web3 library).
Show the balance of an account:
mallet> currentAccount()
'0xfc7e805d72ca57aff872cd010a4c9c5e8e8f22f2'
mallet> getBalance()
'98474579999999000'
mallet> getBalance('0x60950641e7382a120c8825464391da3b84db2a86')
'1000'
Sends a transaction signed with the selected account’s key and returns the transaction hash. This command requires a password to decrypt the private key.
mallet> tx = {
to: '0x60950641e7382a120c8825464391da3b84db2a86', // recipient's address, optional, new contract created if not provided
gas: 100000, // gas limit, mandatory
gasPrice: 5000000000, // gasPrice, optional, default: 5 Gwei
value: 1000000, // optional, default: 0
data: '0xcafebabe' // optional, default: empty
}
mallet> sendTransaction(tx)
Obtains a receipt of a transaction with a given hash. If a hash is not provided, the hash of the most recent transaction will be used.
mallet> sendTransaction(tx)
Enter password:
'0xc71a634009c85640996d64124cf35c3748009c45c952ed456ff5c239ccd5b1d3'
mallet> getReceipt()
null
mallet> getReceipt()
{ transactionHash:
'0xc71a634009c85640996d64124cf35c3748009c45c952ed456ff5c239ccd5b1d3',
transactionIndex: 0,
blockNumber: 295346,
blockHash:
'0x007643f3198296fe6e1b3f125ec603aa758d813cb9ae2c7a2a06daf5357d30bc',
cumulativeGasUsed: 21272,
gasUsed: 21272,
contractAddress: null,
logs: [],
statusCode: '0x00',
status: true,
returnData: [],
rawReturnData: '0xc0' }
mallet> getReceipt('0x00039c02c1ca8cb2b25226f74887fc0afcf485797de65afbc105dab13497ba57')
{ transactionHash:
'0x00039c02c1ca8cb2b25226f74887fc0afcf485797de65afbc105dab13497ba57',
transactionIndex: 0,
blockNumber: 295367,
...
Note that the receipt may not be readily available, indicated with null
value, because it takes time for a transaction to be forged.
If the receipt is for a IELE transaction (technically if it has statusCode
field and the return data is RLP-decodable) then returnData
field contains an array of integers, and raw undecoded data (hex string) is available in rawReturnData
.
This command is different, in that it doesn't interact with the JSON RPC. Instead, it calls the testnet Faucet to obtain funds for a given account. It returns the transaction hash.
mallet> currentAccount()
'0xfc7e805d72ca57aff872cd010a4c9c5e8e8f22f2'
mallet> getBalance()
'972415314998966999'
mallet> requestFunds()
'0x1fc10c0ae70b09fb89fbc39d827c301d05ed0f196f3c54f3c52c4e683b89ff27'
mallet> getBalance()
'1002415314998966999'
mallet> requestFunds('0xd7f3583b8805cfbe0979050f5a1b3587a8fee900')
'0x03c485c980faf11c89d74c92a8d26efda7860edbb7de4a689220e33180a82e6c'
mallet> requestFunds('0xd7f3583b8805cfbe0979050f5a1b3587a8fee900')
Thrown: Faucet error: The user has sent too many requests in a given amount of time.
The following commands are variations of sendTransaction
. They ensure that data is properly encoded for the IELE virtual machine. The argument transaction object is the same as in the case of sendTransaction
, except for the data
field.
This transfers value between accounts Technically, this calls the deposit
function of the IELE virtual machine on the recipient's account.
mallet> getBalance('0xd7f3583b8805cfbe0979050f5a1b3587a8fee900')
'0'
mallet> iele.simpleTransfer({to: '0xd7f3583b8805cfbe0979050f5a1b3587a8fee900', gas: 100000, value: 1000})
Enter password:
'0x1a0ef8a980851ade10f76bcb4b21cd2ad79e13d0b181419a7785ac620fbd2b82'
mallet> getBalance('0xd7f3583b8805cfbe0979050f5a1b3587a8fee900')
'1000'
Creates a contract with the bytecode provided in the code
field, with optional constructor arguments as args
– an array of integers.
mallet> let code = '00000091630369000F696E6372656D656E745828696E742969000667657458282967000000006600003400650002006180016101025511660001F60000660002620101F7016800010001660000340165000201610102541301001C6101025514660001F60000660002620102F7026800020000660000340065000200610101540A6013640001660001F6000103660002620101F701'
undefined
mallet> iele.deployContract({gas: 1000000, value: 0, code: code, args: []})
Enter password:
'0xb13e7783c86dda3f880b2d875201a1d4c4f7f0ae5b085e320dc32a7688ce394d'
mallet> getReceipt()
{ transactionHash:
'0xb13e7783c86dda3f880b2d875201a1d4c4f7f0ae5b085e320dc32a7688ce394d',
transactionIndex: 0,
blockNumber: 36577,
blockHash:
'0x4d65719d5be6ce74ea02f8a59461bde8b831ae02ad72c127e9004b29a394f4c6',
cumulativeGasUsed: 60730,
gasUsed: 60730,
contractAddress: '0x79c7f680aa944545744f611a1a9770426903cee9',
logs: [],
status: '0x00',
returnData: '0xd59479c7f680aa944545744f611a1a9770426903cee9' }
Calls function func
of a contract at the to
address. As with the previous command, there are optional arguments, args
, as an array of integers.
mallet> iele.callContract({to: '0x79c7f680aa944545744f611a1a9770426903cee9', gas: 1000000, func: 'getX()', args: []})
Enter password:
'0x767f44039fbb67503a18688c38576fe0396dc55e18d057b7bef86d9a62fffb57'
mallet> getReceipt()
{ transactionHash:
'0x767f44039fbb67503a18688c38576fe0396dc55e18d057b7bef86d9a62fffb57',
transactionIndex: 0,
blockNumber: 36663,
blockHash:
'0x20d1063fad4453e15f74962ba8d9bc009b4827ca3df6b7dd130a8723f8bb6ebf',
cumulativeGasUsed: 21838,
gasUsed: 21838,
contractAddress: null,
logs: [],
statusCode: '0x00',
status: true,
returnData: [ 0n ],
returnData: '0xc100' }
mallet> iele.callContract({to: '0x79c7f680aa944545744f611a1a9770426903cee9', gas: 1000000, func: 'incrementX(int)', args: [13]})
Enter password:
'0x9592150326c811ad71e55a007b589763c6dbd6365eae2cb66f6532ca780b0799'
mallet> getReceipt()
null
mallet> getReceipt()
{ transactionHash:
'0x9592150326c811ad71e55a007b589763c6dbd6365eae2cb66f6532ca780b0799',
transactionIndex: 0,
blockNumber: 36670,
blockHash:
'0x7daf8a59044c66a1d320043287837b3d1f7d78e2b462e8dc218487cae6d80349',
cumulativeGasUsed: 28263,
gasUsed: 28263,
contractAddress: null,
logs: [],
statusCode: '0x00',
status: true,
returnData: [],
rawReturnData: '0xc0' }
This command is equivalent to web3.eth.call
except with IELE-specific data encoding. It can be used for calling contract functions that do not change the state (Solidity's view
functions). No account has to be selected to run this command. The TX argument is the same as for iele.callContract
. The command returns decoded array of integers comprising the function return data.
mallet> iele.constantCall({to: '0x79c7f680aa944545744f611a1a9770426903cee9', gas: 1000000, func: 'getX()', args: []})
[ 0n ]
Solidity functions compiled to IELE have a naming convention that includes function the original function name along with its argument types (this is to support function overloading). Examples
Solidity function header | IELE function name |
---|---|
function getX() returns (int) |
getX() |
`function incrementX(int i) | `incrementX(int) |
function somethingComplex(address a, bytes b, int[] i) returns (string, int) |
somethingComplex(address,bytes,int[]) |
IELE functions by design accept and return array of unbounded integers. To encode/decode different Solidity to/from integers use iele.enc
/iele.dec
. Both functions take a value to be converted and a Solidity type as a string. Consider a Solidity function like this:
function dummyFunc(address a, bytes b, int[] i) public pure returns (string, int) {
return ("I'm a dummy", i[0]);
}
Here's how you can use iele.enc
and iele.dec
:
mallet> contractAddress = '0x9785367f32a97ec34090307368a4368f2ab4bc01'
'0x9785367f32a97ec34090307368a4368f2ab4bc01'
mallet> args = [iele.enc(contractAddress, 'address'), iele.enc('0xcafebabe', 'bytes'), iele.enc([42, -1], 'int[]')]
[ 865028352852446724060954038272434378925942291457n,
4222355708056220710451082685618494097063946n,
1455792646560079078679811948732730198604464062977n ]
mallet> result = iele.constantCall({to: contractAddress, gas: 1000000, func: 'dummyFunc(address,bytes,int[])', args: args})
[ 1631388912461674177904633800030782296862752779n, 42n ]
mallet> iele.dec(result[0], 'string')
'I\'m a dummy'
mallet> iele.dec(result[1], 'int')
42
Compiling contracts is currently only supported for IELE, and only for a single source file. Both IELE assembly language and Solidity (using a Solidity-to-IELE compiler) contracts can be compiled. Both compilers are services of the testnet that Mallet connects to (there are no additional dependencies).
Sends the source code from the provided file path to the compiler. The mode of compilation is determined by the extension of the source file:
.sol
: two-step compilation: Solidity to IELE and then to IELE assembly.iele
: direct to IELE assembly
mallet> iele.compile('test/contracts/sendEther.sol')
{ source: '// Simple contract ...'
solidityCompilerOutput: 'Warning: This is a pre-release compiler version...',
error: false,
ieleCode: 'contract "main.sol:test" ...',
bytecode: '000000AA630469000F612...' }
The compiled contract is available in the bytecode
property, which can then be used as an argument to iele.createContract
. In the case of a compilation failure, the error
property will be set to true
and the relevant compiler output can be found in solidityCompilerOutput
.
With direct IELE assembly compilation, the compiler interface is simpler:
mallet> iele.compile('test/contracts/sendEther.iele')
{ source: 'contract "sendEther" {\n\ndefine @init() ...',
result: '000000AA630469000...' }
The result
property will contain either the correctly compiled bytecode, or textual error information.
To use Mallet as library add:
const Mallet = require('@iohk/mallet');
in your script. See test/basic-kevm.js for an example of using Mallet in a script.
As mentioned before, the IELE testnet pages have detailed instructions and a rather good video:
-
Installing Node.js and Mallet, plus a tutorial from: https://testnet.iohkdev.io/iele/
-
Getting Started with Mallet video: https://www.youtube.com/watch?v=Tp4Z0RbjSa8
-
The Mallet repository is here: https://github.com/input-output-hk/mallet
-
IOHK provides support on using IELE and the testnet. Details at: https://testnet.iohkdev.io/iele/resources/support-and-help/