There are so many Ethereum Oracles out there. But I needed a more simple and straight forward one for my projects. Thus, I created my own one. Hi, I'm CaptainJS and my nodejs container ship just left the harbor. Be my Seaman and start invoking JavaScript directly from Ethereum's Solidity. Here's how to do it...
The Captain needs 0.55 ETH every month to keep his kubernetes containership up and running. Feel free to donate to Mainnet-Address 0xc52f52057f6bf5ecc75d704cb85510f4e1ba207b.
The SeamansExamples contract contains some code which you will see in the further chapters. There's a demo instance running at Ethereum's mainnet at address 0xfcd53089c3de49fa8c6cc8330cd9f49e84b01cd6 (ROPSTEN: 0x2c53859c18da0e286161f1649e6a5fdabcb9bb98) waiting for your tests.
In some cases it is necessary to have a mechanism to call the contract back. For example a fond which withdraws its budget every year to specific people. But Solidity can't call itself.
Therefore extend from usingCaptainJS and start coding...
Use a unique integer job Id to identify your callback. Invoke RingShipsBell with enough gas and receive the callback in method RingRing. That's it.
To make sure that RingRing will only be called by the captain himself and not a pirate, add onlyCaptainsOrdersAllowed to its declaration.
At the moment the captain can only accept bell rings with a maximum of 48 hours.
contract SeamansExamples is usingCaptainJS {
...
uint constant SHIPS_BELL_RING = 3;
function CallbackExample() public {
RingShipsBell(
SHIPS_BELL_RING, /* give the job a unique ID */
20, /* minutes from now */
DEFAULT_GAS_UNITS, /* use default gas */
DEFAULT_GAS_PRICE /* use default gas price */
);
}
function RingRing(uint UniqueIdentifier) external onlyCaptainsOrdersAllowed {
// OK. It worked and we got a result
...
}
Now let's look at a simple JavaScript job. JavaScript's mathjs library has multiple useful functions such as the conversion of centimeter to inch.
In order to use this library you need to call the Run function and hand over the JavaScript code that is necessary to convert centimeters to inch.
The JavaScript code you want to submit must be written the following way:
module.exports = function(CaptainJSIn) { /* here goes your code */ }
CaptainJS will invoke your code within a container by calling the default function. CaptainJSIn will contain your inputs of the JavaScript function. Then CaptainJS will return the result of your code. If your JavaScript code was successful: CaptainsResult will be invoked. The return result is always a string.
If your JavaScript code was not successful or its result couldn't be send back (it failed or there was not enough gas or whatever happened) then CaptainsError will be invoked.
To make sure that both CaptainsResult and CaptainsError will only be called by the captain himself and not a pirate, add onlyCaptainsOrdersAllowed to its declaration.
A runtime slice has a duration of 10 seconds and it includes the download and install of all required npm modules. At the moment 6 runtime slices are the maximum.
Here's the complete code snippet:
contract SeamansExamples is usingCaptainJS {
...
uint constant CENTIMETER_JOB = 1;
function CentimeterToInchExample(string Centimeter) public {
Run(
CENTIMETER_JOB, /* give the job a unique ID */
/* JavaScript code I want to execute: */
"module.exports = function(CaptainJSIn) { var math = require('mathjs'); return math.eval(CaptainJSIn + ' cm to inch'); }",
Centimeter, /* Input parameter which will result in CaptainJSIn (see above) */
"mathjs", /* Nodejs libraries we need */
/* we need a maximum of 2 runtime slices */
3, /* we need a maximum of 3 runtime slices */
DEFAULT_GAS_UNITS, /* use default gas units */
DEFAULT_GAS_PRICE /* we will transfer the default gas price for return */
);
}
function CaptainsResult(uint UniqueJobIdentifier, string Result) external onlyCaptainsOrdersAllowed {
// analyse the return results
if(UniqueJobIdentifier == CENTIMETER_JOB) {
// OK. It worked and we got a result
...
} else
if(UniqueJobIdentifier == WOLFRAMALPHA_JOB) {
// OK. It worked and we got another result
...
}
}
function CaptainsError(uint UniqueJobIdentifier, string Error) external onlyCaptainsOrdersAllowed {
// analyse the return results
if(UniqueJobIdentifier == CENTIMETER_JOB) {
// OK. It didn't work :-/
}
}
}
(I have a special mathjs function in use case #6)
Now let's look at a more complex JavaScript job. You want to ask WolframAlpha anything it knows about a country like France. Therefore you design your code the same way like you did in Use Case #2.
To query WolframAlpha you use JavaScript's axios library. The default function must be async so that you can wait for a result when you invoke axios.get(...). axios will return a JSON object but we need to flatten it to a string:
module.exports = async function(CaptainJSIn) {
const axios = require('axios');
const WAlpha = await axios.get('http://www.wolframalpha.com/queryrecognizer/query.jsp?appid=DEMO&mode=Default&i=' + CaptainJSIn + '&output=json');
return JSON.stringify(WAlpha.data);
}
Again, CaptainJS will invoke your code within a container by calling this default function. CaptainJSIn will contain your inputs such as "France".
Again, if your JavaScript code was successful: CaptainsResult will be invoked. Otherwise CaptainsError will be invoked.
And because Solidity sometimes is such a crappy programming language you will use a very expensive concat function to make your JavaScript code more readable.
Here's the complete code snippet.
contract SeamansExamples is usingCaptainJS {
...
uint constant WOLFRAMALPHA_JOB = 2;
function WolframAlphaExample(string Country) public {
Run(
WOLFRAMALPHA_JOB, /* give the job a unique ID */
concat ( /* JavaScript code I want to execute: */
"module.exports = async function(CaptainJSIn) { ",
" const axios = require('axios'); ",
" const WAlpha = await axios.get('http://www.wolframalpha.com/queryrecognizer/query.jsp?appid=DEMO&mode=Default&i=' + CaptainJSIn + '&output=json'); ",
" return JSON.stringify(WAlpha.data); ",
"}"
),
Country, /* Input parameter which will result in CaptainJSIn (see above) */
"axios, mathjs", /* Nodejs libraries we need */
3, /* we need a maximum of 3 runtime slices */
200000, /* use 200,000 gas units */
DEFAULT_GAS_PRICE /* use default gas price */
);
}
...
}
A classic oracle request is a simple JSON, XML/XPath or HTML/jQuery request. Instead of writing a full-blown JavaScript code which does the query for you the newest release has predefined queries.
To invoke a simple query just use the Run method in the same way as you did it in the previous 2 use cases. But instead of submitting JavaScript code send an URL that has either a html:, xml: or json: prefix. The input parameter of the Run method is either a JSON, XPath or jQuery expression. Typically 1 runtime slice is enough.
If your JavaScript code was successful: CaptainsResult will be invoked. Otherwise CaptainsError will be invoked.
(The client libraries now include a test module to see if your code works - before you submitted to the blockchain)
function HTMLqueryExample() public {
Run(
HTML_QUERY_EXAMPLE, /* give the job a unique ID */
/* url needs to start with html: */
"html:http://www.amazon.co.uk/gp/product/1118531647",
/* Input parameter is the jQuery. Result will be stored in QUERY_RESULT variable */
"$('span.inlineBlock-display span.a-color-price').each(function(i, element) {var el = $(this); QUERY_RESULT = el.text(); })",
"", /* no modules required */
1, /* queries are fast */
DEFAULT_GAS_UNITS, /* use default gas units */
DEFAULT_GAS_PRICE /* we will transfer the default gas price for return */
);
}
function JSONqueryExample() public {
Run(
JSON_QUERY_EXAMPLE, /* give the job a unique ID */
/* url needs to start with json: */
"json:https://api.kraken.com/0/public/Ticker?pair=ETHUSD",
/* Input parameter is the JSON path */
"result.XETHZUSD.a[0]",
"", /* no modules required */
1, /* queries are fast */
DEFAULT_GAS_UNITS, /* use default gas units */
DEFAULT_GAS_PRICE /* we will transfer the default gas price for return */
);
}
For those seamen who like to encrypt their data which is stored in the blockchain I've added an encryption module to the latest release. This allows you to send encrypted data to the Captain's NodeJS containers. There it will be decrypted and executed. (The string result will not be send encrypted back)
For example you want to send a mail after a transaction happened then you could make use of nodemailer, login to your mail account and send the mail. In this case your code will look similar to this one:
module.exports = async function(CaptainJSIn) {
var nodemailer = require("nodemailer");
var transport = nodemailer.createTransport({
host: "smtp-mail.outlook.com", // hostname
secureConnection: false, // TLS requires secureConnection to be false
port: 587, // port for secure SMTP
tls: {
ciphers:'SSLv3'
},
auth: {
user: "myaddress@outlook.com",
pass: "mysecretpassword"
}
});
var mailOptions = {
to: 'donald.trum@whitehouse.gov', // list of receivers
subject: "you're fired", // Subject line
text: "Dear Donald, ..."
};
// send mail with defined transport object
transport.sendMail(mailOptions, function(error, info){});
}
The bad side of the story is, that your account details will be stored within your code. And these account details will be stored in Ethereum's blockchain forever. That's bad. But there's hope!
In the client libraries I've added the https://github.com/CaptainJavaScript/Seaman-Client/blob/master/CaptainJS-Encryption.js which help you to encrypt your data before you include them in your contract:
- just simply add your module to a file like MailSample.js
- encrypt the file using EncryptFile from CaptainJS-Encryption.js
- add the encrypted code to your contract
Encrypt your code:
var ENC = require("./CaptainJS-Encryption.js");
async function RUN() {
await ENC.EncryptFile(false, "MailSample.js", "EncryptedMailSample.txt",
() => { console.log("Success!"); },
(ERROR) => { console.log(ERROR); }
);
}
RUN();
And add your encrypted code to the Solidity-based contract:
function HTMLqueryExample() public {
Run(
ENCRYPTED_MAIL_EXAMPLE,
"crypt:8366268bd167a9f8318f99c71d0f489d0372b545735c2e10303c47bad2507e933171f72f...",
"",
"",
1,
DEFAULT_GAS_UNITS,
DEFAULT_GAS_PRICE
);
}
The Captain will never read your code! It just gets executed in a fresh new container. In other words: if you encrypt your data then it will stay encrypted until it's executed in a container (and the container will vanish after execution). The encryption uses the public/private key of the Captain's Ethereum contracts. This is the only way nobody else can decrypt your data. Allthough the captain could read your encrypted data for private purposes, he doesn't do it. That's a promise and a proof of trust.
I'm an old man and I remember the old Basic meets Assembler days that made my hair become grey. Math in Solidity is hell. Why not simply use MathJS? Why re-code everything what already exists??? Here's the trick:
contract MathDemo is usingCaptainJS {
...
function DoLog2_Example() public {
Run(
1, /* give the job a unique ID */
"math:log2(16)", /* just call the mathjs-function after math: */
"",
"",
1, /* it's fast: 1 slice is enough */
DEFAULT_GAS_UNITS, /* use default gas units */
DEFAULT_GAS_PRICE /* we will transfer the default gas price for return */
);
}
function CaptainsResult(uint UniqueJobIdentifier, string Result) external onlyCaptainsOrdersAllowed {
// continue with the Log2-result
uint Log2Result = parseInt(Result);
...
}
...
}
Sometimes it is too much effort for me to write a full JavaScript module. Sometimes I just want to execute 1-line of code.
Here's how to do it:
contract MathDemo is usingCaptainJS {
...
function DoLog2_Example() public {
Run(
1, /* give the job a unique ID */
"eval:var a = CaptainJSIn + 2; CaptainJSOut = a;", /* just add eval: keyword */
"",
"",
1, /* it's fast: 1 slice is enough */
DEFAULT_GAS_UNITS, /* use default gas units */
DEFAULT_GAS_PRICE /* we will transfer the default gas price for return */
);
}
...
}
The captain then will transform this into a JavaScript module before he's executing it:
module.exports = async function(CaptainJSIn) {
var CaptainJSOut = '';
var a = 1 + 2; CaptainJSOut = a;return CaptainJSOut;
}
If you want to upload or download files to an IPFS server than just use the ipfs: command:
...
Run(
jobId,
"ipfs:add:*server_url*:*server_port*:*base64_encoded_file_content*",
"*file_name*",
...
);
and:
...
Run(
jobId,
"ipfs:cat:*server_url*:*server_port*:*hash_value*",
"",
...
);
The :add: command will add the file to the selected IPFS drive. The 🐱 command will get the file.
There's both a ROPSTEN and MAINNET version available. Both are equal. Inside your usingCaptainJS there is a voucher context included. Just add your voucher code by calling ActivateVoucher from your derived Solidity code.
Just check Captain's Twitter account for vouchers.
function UseVoucher() public {
ActivateVoucher("MobyDick");
}
The Captain just rented his container ship. To pay his ship he needed to set these prices. Prices may change over time and will be updated both here + via Twitter.
- RingShipsBell(...):
- if you have a voucher code then you transfer your gas budget for the callback
- otherwise the full price needs to be payed
- PricePerSubmission = 20 szabo;
- PricePerBellRing = 6 szabo;
if(HasVoucher)
return GasForCallback * GasPriceInWei;
else
return PricePerBellRing + PricePerSubmission + (GasForCallback * GasPriceInWei);
- Run(...):
- if you have a promo code then you transfer your gas budget for the callback
- otherwise the full price needs to be payed
- PricePerSubmission = 20 szabo;
- PricePerSlice = 50 szabo; // 1 slice = 10 seconds
if(HasVoucher)
return GasForCallback * GasPriceInWei;
else
return PricePerSubmission + (RuntimeSlices * PricePerSlice) + (GasForCallback * GasPriceInWei);
-
what if you don't transfer enough gas?
- then your job won't fail directly.
- the captain will try his best to get the job back on the blockchain
- he will calculate the budget you submitted when calling Run(...) as budget = 350000 * max possible gas_price
- if your job result then still doesn't go through, then good night
-
what if you transfer too much gas and a too high gas price?
- then the captain will order a bottle of rum on your expense
-
what if it a callback is delayed (ie. it is invoked after 2 days instead of 2 hours)
- the captain then will argue that the concept of blockchain isn't made for exacted invocations
- be mentally prepared then
- transfer a higher budget next time by increasing gas and gas price
-
what if a JavaScript code never gets executed?
- relax
- the captain then will shout "impossible!" and try its best by informing you via CaptainsError invocation
-
what if you change code within usingCaptainJS?
- you won't be keelhauled :-)
- try it, improve it!
-
what if you submit bad code?
- we don't like pirates!
- this is not nice!
I uploaded a JavaScript test client at https://github.com/CaptainJavaScript/Seaman-Client