Last active
April 19, 2018 13:41
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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