Skip to content

Instantly share code, notes, and snippets.

@akirattii
Last active April 19, 2018 13:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save akirattii/dcf76564e688500e1853063cdf3fd78a to your computer and use it in GitHub Desktop.
Save akirattii/dcf76564e688500e1853063cdf3fd78a to your computer and use it in GitHub Desktop.
Ripple: JSON RPC commandline tool for rippled. By just one command, you can invisible-sign locally for secure then submit signed tx immediately.
#!/usr/bin/env node
/**
* Ripple CLI tool
*
* The MIT License (MIT)
* Copyright (c) 2017 akirattii <tanaka.akira.2006@gmail.com> (http://mint.pepper.jp)
*/
/**
# Usage & example:
### example1 (no sign required):
```
$ node ripple-cli.js --data '''{
"method": "account_info",
"params": [{
"account": "rHDvn877ezKZokt9WWeVhjwfab53o4pjw1"
}]
}''' --url https://api.altnet.rippletest.net:51234
```
### example2 (tx sign required):
```
$ node ripple-cli.js --data '''{
"TransactionType" : "Payment",
"Account" : "rHDvn877ezKZokt9WWeVhjwfab53o4pjw1",
"Destination" : "rcSFxB5UoDKFjfoa5HtQqgqKcjkd2N4ah",
"Amount" : {
"currency" : "XXX",
"value" : "0.1",
"issuer" : "rLoohEFbCnV9tgtAxcuzhxA2uUu7N77jbn"
},
"Fee": "100",
"Flags": 2147483648
}''' --url https://api.altnet.rippletest.net:51234
? Input your secret seed: [hidden] # prompt your secret here
```
It signs locally tx which contains "TransactionType" property then submits txhex immediately.
Above example does not set `Sequence` property but it is filled automatically.
*/
const DEFAULT_SERVER = "https://api.altnet.rippletest.net:51234"; // testnet
const argv = require('minimist')(process.argv.slice(2));
const Keypairs = require('ripple-keypairs');
const Binary = require('ripple-binary-codec');
const { computeBinaryTransactionHash } = require('ripple-hashes');
const request = require('request');
const inquirer = require('inquirer');
const Step = require("step");
const winston = require("winston");
const logger = new(winston.Logger)({
transports: [
new(winston.transports.Console)({
level: 'info',
prettyPrint: true,
colorize: true,
timestamp: true,
})
]
});
// parameter check
var data;
try {
checkURL(argv["url"]);
data = parseData(argv["data"]);
} catch (e) {
return logger.error(e.message);
}
if (!argv["url"]) {
logger.warn("--url option has not be set. use default:", DEFAULT_SERVER);
argv["url"] = DEFAULT_SERVER;
}
const url = argv["url"];
const shouldSign = data["TransactionType"] === undefined ? false : true
// create req param:
if (shouldSign === true) {
quest(data, (err, data) => {
if (err) return finish(err, data);
execute({ data }, finish);
});
} else {
execute({ data }, finish);
}
function quest(data, cb) {
const questions = [{
type: "password",
name: "secret",
message: "Input your secret seed:",
}];
// quest secret using inquirer
inquirer.prompt(questions).then(function(answers) {
// logger.info("answers", answers);
if (!isValidSecret(answers["secret"]))
return cb && cb('invalid secret seed');
createSubmitData({ data, secret: answers["secret"] }, cb);
});
}
function createSubmitData({ data, secret }, cb) {
Step(function(err) {
if (err) throw err;
getAccountInfo(data["Account"], this);
}, function(err, v) {
if (err) return cb && cb(err);
const offset = 2;
data["Sequence"] = v["account_data"]["Sequence"];
data["LastLedgerSequence"] = v["ledger_current_index"] + offset;
// pre-sign
// logger.info("pre-sign tx:", data);
const signedTxObj = sign(data, secret);
const ret = {
method: "submit",
params: [{
tx_blob: signedTxObj.signedTx,
}]
};
return cb && cb(err, ret);
});
}
function getAccountInfo(account, cb) {
const data = {
method: "account_info",
params: [{ account }]
};
execute({ data }, (err, res) => {
if (err) throw err;
return cb && cb(err, res);
});
}
function execute({ data }, cb) {
const reqParams = createReqParam({ data });
req(reqParams, cb);
}
function finish(err, result) {
if (err) {
return logger.error("error occurred:", err);
}
logger.info("**********************************************************");
logger.info(" Response");
logger.info("**********************************************************");
logger.info(result);
}
function parseData(s) {
try {
return JSON.parse(s);
} catch (e) {
throw e;
}
}
function checkURL(s) {
if (!s) return;
if (/^(ws[s]?|http[s]?):\/\/[a-zA-Z0-9]+/.test(s) == false)
throw Error(`invalid url: ${s}
Typically you can use below rippled server:
https://s1.ripple.com:51234 (General purpose server)
https://s2.ripple.com:51234 (Full-history server)`);
}
function createReqParam({ data }) {
let form = JSON.stringify(data);
logger.info(` Requesting data:`, data);
let p = {
url,
headers: {
"Content-Type": "application/json; charset=UTF-8",
"Accept": "application/json, text/javascript",
},
form,
method: "POST", // JSONRPC server handles only POST requests
encoding: "UTF-8",
}
return p;
}
function req({
url,
method = "POST",
headers,
json = true,
jar,
form,
encoding = "UTF-8",
}, cb) {
if (!url) throw Error(`param 'url' must be set`);
let p = {
url,
method,
headers,
json,
jar,
form,
encoding,
};
logger.info("Calling ", url);
request(p, (err, response, body) => {
if (err) return cb && cb(err, body);
if (!body) {
err = Error(`no response`);
err.code = response.statusCode;
}
if (response.statusCode != 200) {
err = Error(body);
err.code = response.statusCode;
}
let result;
if (body.error) {
err = Error(body.error);
err.code = response.statusCode;
result = body;
} else {
result = body.result;
}
return cb && cb(err, result);
});
}
function sign(tx, secret, options = {}) {
if (tx.TxnSignature || tx.Signers) {
throw Error('tx must not contain "TxnSignature" or "Signers" properties');
}
const keypair = Keypairs.deriveKeypair(secret);
tx.SigningPubKey = options.signAs ? '' : keypair.publicKey;
if (options.signAs) {
const signer = {
Account: options.signAs,
SigningPubKey: keypair.publicKey,
TxnSignature: computeSignature(tx, keypair.privateKey, options.signAs),
}
tx.Signers = [{ Signer: signer }];
} else {
tx.TxnSignature = computeSignature(tx, keypair.privateKey);
}
const serialized = Binary.encode(tx);
return {
signedTx: serialized,
id: computeBinaryTransactionHash(serialized),
};
};
function computeSignature(tx, privateKey, signAs) {
const signingData = signAs ?
Binary.encodeForMultisigning(tx, signAs) : Binary.encodeForSigning(tx);
return Keypairs.sign(signingData, privateKey)
}
function isValidSecret(secret) {
try {
Keypairs.deriveKeypair(secret);
return true;
} catch (err) {
return false;
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment