mirror of
https://github.com/Xahau/xahau.js.git
synced 2026-07-30 18:40:21 +00:00
Add multisignature support
This commit is contained in:
@@ -44,6 +44,7 @@ const prepareSuspendedPaymentCancellation =
|
||||
require('./transaction/suspended-payment-cancellation');
|
||||
const prepareSettings = require('./transaction/settings');
|
||||
const sign = require('./transaction/sign');
|
||||
const combine = require('./transaction/combine');
|
||||
const submit = require('./transaction/submit');
|
||||
const errors = require('./common').errors;
|
||||
const generateAddress =
|
||||
@@ -125,6 +126,7 @@ _.assign(RippleAPI.prototype, {
|
||||
prepareSuspendedPaymentCancellation,
|
||||
prepareSettings,
|
||||
sign,
|
||||
combine,
|
||||
submit,
|
||||
|
||||
generateAddress,
|
||||
|
||||
@@ -94,7 +94,8 @@ function loadSchemas() {
|
||||
require('./schemas/input/compute-ledger-hash'),
|
||||
require('./schemas/input/sign.json'),
|
||||
require('./schemas/input/submit.json'),
|
||||
require('./schemas/input/generate-address.json')
|
||||
require('./schemas/input/generate-address.json'),
|
||||
require('./schemas/input/combine.json')
|
||||
];
|
||||
const titles = _.map(schemas, schema => schema.title);
|
||||
const duplicates = _.keys(_.pick(_.countBy(titles), count => count > 1));
|
||||
|
||||
19
src/common/schemas/input/combine.json
Normal file
19
src/common/schemas/input/combine.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"title": "combineParameters",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"signedTransactions": {
|
||||
"type": "array",
|
||||
"description": "An array of signed transactions (from the output of [sign](#sign)) to combine.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-F0-9]+$",
|
||||
"description": "A single-signed transaction represented as an uppercase hexadecimal string (from the output of [sign](#sign))"
|
||||
},
|
||||
"minLength": 1
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": ["signedTransactions"]
|
||||
}
|
||||
@@ -11,6 +11,17 @@
|
||||
"type": "string",
|
||||
"format": "secret",
|
||||
"description": "The secret of the account that is initiating the transaction."
|
||||
},
|
||||
"options": {
|
||||
"type": "object",
|
||||
"description": "Options that control the type of signature that will be generated.",
|
||||
"properties": {
|
||||
"signAs": {
|
||||
"$ref": "address",
|
||||
"description": "The account that the signature should count for in multisigning."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -68,6 +68,35 @@
|
||||
],
|
||||
"description": "The public key of a new keypair, to use as the regular key to this account, as a base-58-encoded string in the same format as an account address. Use `null` to remove the regular key."
|
||||
},
|
||||
"signers": {
|
||||
"type": "object",
|
||||
"description": "Settings that determine what sets of accounts can be used to sign a transaction on behalf of this account using multisigning.",
|
||||
"properties": {
|
||||
"threshold": {
|
||||
"$ref": "uint32",
|
||||
"description": "A target number for the signer weights. A multi-signature from this list is valid only if the sum weights of the signatures provided is equal or greater than this value. To delete the signers setting, use the value `0`."
|
||||
},
|
||||
"weights": {
|
||||
"type": "array",
|
||||
"description": "Weights of signatures for each signer.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"description": "An association of an address and a weight.",
|
||||
"properties": {
|
||||
"address": {"$ref": "address"},
|
||||
"weight": {
|
||||
"$ref": "uint32",
|
||||
"description": "The weight that the signature of this account counts as towards the threshold."
|
||||
}
|
||||
},
|
||||
"required": ["address", "weight"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"minItems": 1,
|
||||
"maxItems": 8
|
||||
}
|
||||
}
|
||||
},
|
||||
"memos": {"$ref": "memos"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
||||
@@ -47,6 +47,7 @@ module.exports = {
|
||||
prepareSuspendedPaymentExecution: _.partial(schemaValidate,
|
||||
'prepareSuspendedPaymentExecutionParameters'),
|
||||
sign: _.partial(schemaValidate, 'signParameters'),
|
||||
combine: _.partial(schemaValidate, 'combineParameters'),
|
||||
submit: _.partial(schemaValidate, 'submitParameters'),
|
||||
computeLedgerHash: _.partial(schemaValidate, 'computeLedgerHashParameters'),
|
||||
generateAddress: _.partial(schemaValidate, 'generateAddressParameters'),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
const _ = require('lodash');
|
||||
const BigNumber = require('bignumber.js');
|
||||
const AccountFields = require('./utils').constants.AccountFields;
|
||||
|
||||
@@ -22,6 +23,26 @@ function parseFields(data: Object): Object {
|
||||
settings[info.name] = parseField(info, fieldValue);
|
||||
}
|
||||
}
|
||||
|
||||
if (data.RegularKey) {
|
||||
settings.regularKey = data.RegularKey;
|
||||
}
|
||||
|
||||
// TODO: this isn't implemented in rippled yet, may have to change this later
|
||||
if (data.SignerQuorum || data.SignerEntries) {
|
||||
settings.signers = {};
|
||||
if (data.SignerQuorum) {
|
||||
settings.signers.threshold = data.SignerQuorum;
|
||||
}
|
||||
if (data.SignerEntries) {
|
||||
settings.signers.weights = _.map(data.SignerEntries, entry => {
|
||||
return {
|
||||
address: entry.SignerEntry.Account,
|
||||
weight: entry.SignerEntry.SignerWeight
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
|
||||
|
||||
@@ -52,10 +52,10 @@ function parseFlags(tx: Object) {
|
||||
|
||||
function parseSettings(tx: Object) {
|
||||
const txType = tx.TransactionType;
|
||||
assert(txType === 'AccountSet' || txType === 'SetRegularKey');
|
||||
assert(txType === 'AccountSet' || txType === 'SetRegularKey' ||
|
||||
txType === 'SignerListSet');
|
||||
|
||||
const regularKey = tx.RegularKey ? {regularKey: tx.RegularKey} : {};
|
||||
return _.assign(regularKey, parseFlags(tx), parseFields(tx));
|
||||
return _.assign({}, parseFlags(tx), parseFields(tx));
|
||||
}
|
||||
|
||||
module.exports = parseSettings;
|
||||
|
||||
@@ -22,7 +22,8 @@ function parseTransactionType(type) {
|
||||
SetRegularKey: 'settings',
|
||||
SuspendedPaymentCreate: 'suspendedPaymentCreation',
|
||||
SuspendedPaymentFinish: 'suspendedPaymentExecution',
|
||||
SuspendedPaymentCancel: 'suspendedPaymentCancellation'
|
||||
SuspendedPaymentCancel: 'suspendedPaymentCancellation',
|
||||
SignerListSet: 'settings'
|
||||
};
|
||||
return mapping[type] || null;
|
||||
}
|
||||
|
||||
39
src/transaction/combine.js
Normal file
39
src/transaction/combine.js
Normal file
@@ -0,0 +1,39 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
const _ = require('lodash');
|
||||
const binary = require('ripple-binary-codec');
|
||||
const utils = require('./utils');
|
||||
const BigNumber = require('bignumber.js');
|
||||
const {decodeAddress} = require('ripple-address-codec');
|
||||
const {validate} = utils.common;
|
||||
const {computeBinaryTransactionHash} = require('ripple-hashes');
|
||||
|
||||
function addressToBigNumber(address) {
|
||||
const hex = (new Buffer(decodeAddress(address))).toString('hex');
|
||||
return new BigNumber(hex, 16);
|
||||
}
|
||||
|
||||
function compareSigners(a, b) {
|
||||
return addressToBigNumber(a.Signer.Account)
|
||||
.comparedTo(addressToBigNumber(b.Signer.Account));
|
||||
}
|
||||
|
||||
function combine(signedTransactions: Array<string>): Object {
|
||||
validate.combine({signedTransactions});
|
||||
|
||||
const txs = _.map(signedTransactions, binary.decode);
|
||||
const tx = _.omit(txs[0], 'Signers');
|
||||
if (!_.every(txs, _tx => _.isEqual(tx, _.omit(_tx, 'Signers')))) {
|
||||
throw new utils.common.errors.ValidationError(
|
||||
'txJSON is not the same for all signedTransactions');
|
||||
}
|
||||
const unsortedSigners = _.reduce(txs, (accumulator, _tx) =>
|
||||
accumulator.concat(_tx.Signers || []), []);
|
||||
const signers = unsortedSigners.sort(compareSigners);
|
||||
const signedTx = _.assign({}, tx, {Signers: signers});
|
||||
const signedTransaction = binary.encode(signedTx);
|
||||
const id = computeBinaryTransactionHash(signedTransaction);
|
||||
return {signedTransaction, id};
|
||||
}
|
||||
|
||||
module.exports = combine;
|
||||
@@ -70,7 +70,17 @@ function convertTransferRate(transferRate: number | string): number | string {
|
||||
return (new BigNumber(transferRate)).shift(9).toNumber();
|
||||
}
|
||||
|
||||
function createSettingsTransaction(account: string, settings: Settings
|
||||
function formatSignerEntry(signer: Object): Object {
|
||||
return {
|
||||
SignerEntry: {
|
||||
Account: signer.address,
|
||||
SignerWeight: signer.weight
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createSettingsTransactionWithoutMemos(
|
||||
account: string, settings: Settings
|
||||
): Object {
|
||||
if (settings.regularKey !== undefined) {
|
||||
const removeRegularKey = {
|
||||
@@ -83,15 +93,20 @@ function createSettingsTransaction(account: string, settings: Settings
|
||||
return _.assign({}, removeRegularKey, {RegularKey: settings.regularKey});
|
||||
}
|
||||
|
||||
if (settings.signers !== undefined) {
|
||||
return {
|
||||
TransactionType: 'SignerListSet',
|
||||
Account: account,
|
||||
SignerQuorum: settings.signers.threshold,
|
||||
SignerEntries: _.map(settings.signers.weights, formatSignerEntry)
|
||||
};
|
||||
}
|
||||
|
||||
const txJSON: Object = {
|
||||
TransactionType: 'AccountSet',
|
||||
Account: account
|
||||
};
|
||||
|
||||
if (settings.memos !== undefined) {
|
||||
txJSON.Memos = _.map(settings.memos, utils.convertMemo);
|
||||
}
|
||||
|
||||
setTransactionFlags(txJSON, _.omit(settings, 'memos'));
|
||||
setTransactionFields(txJSON, settings);
|
||||
|
||||
@@ -101,6 +116,15 @@ function createSettingsTransaction(account: string, settings: Settings
|
||||
return txJSON;
|
||||
}
|
||||
|
||||
function createSettingsTransaction(account: string, settings: Settings
|
||||
): Object {
|
||||
const txJSON = createSettingsTransactionWithoutMemos(account, settings);
|
||||
if (settings.memos !== undefined) {
|
||||
txJSON.Memos = _.map(settings.memos, utils.convertMemo);
|
||||
}
|
||||
return txJSON;
|
||||
}
|
||||
|
||||
function prepareSettings(address: string, settings: Settings,
|
||||
instructions: Instructions = {}
|
||||
): Promise<Prepare> {
|
||||
|
||||
@@ -6,23 +6,38 @@ const binary = require('ripple-binary-codec');
|
||||
const {computeBinaryTransactionHash} = require('ripple-hashes');
|
||||
const validate = utils.common.validate;
|
||||
|
||||
function computeSignature(txJSON, privateKey) {
|
||||
const signingData = binary.encodeForSigning(txJSON);
|
||||
function computeSignature(tx: Object, privateKey: string, signAs: ?string) {
|
||||
const signingData = signAs ?
|
||||
binary.encodeForMultisigning(tx, signAs) : binary.encodeForSigning(tx);
|
||||
return keypairs.sign(signingData, privateKey);
|
||||
}
|
||||
|
||||
function sign(txJSON: string, secret: string
|
||||
function sign(txJSON: string, secret: string, options: Object = {}
|
||||
): {signedTransaction: string; id: string} {
|
||||
validate.sign({txJSON, secret});
|
||||
// we can't validate that the secret matches the account because
|
||||
// the secret could correspond to the regular key
|
||||
|
||||
const tx = JSON.parse(txJSON);
|
||||
const keypair = keypairs.deriveKeypair(secret);
|
||||
if (tx.SigningPubKey === undefined) {
|
||||
tx.SigningPubKey = keypair.publicKey;
|
||||
if (tx.TxnSignature || tx.Signers) {
|
||||
throw new utils.common.errors.ValidationError(
|
||||
'txJSON must not contain "TxnSignature" or "Signers" properties');
|
||||
}
|
||||
tx.TxnSignature = computeSignature(tx, keypair.privateKey);
|
||||
|
||||
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 {
|
||||
signedTransaction: serialized,
|
||||
|
||||
@@ -3,15 +3,7 @@
|
||||
const _ = require('lodash');
|
||||
const utils = require('./utils');
|
||||
const {validate} = utils.common;
|
||||
|
||||
type Submit = {
|
||||
success: boolean,
|
||||
engineResult: string,
|
||||
engineResultCode: number,
|
||||
engineResultMessage?: string,
|
||||
txBlob?: string,
|
||||
txJson?: Object
|
||||
}
|
||||
import type {Submit} from './types.js';
|
||||
|
||||
function isImmediateRejection(engineResult: string): boolean {
|
||||
// note: "tel" errors mean the local server refused to process the
|
||||
@@ -23,7 +15,7 @@ function isImmediateRejection(engineResult: string): boolean {
|
||||
return _.startsWith(engineResult, 'tem') || _.startsWith(engineResult, 'tej');
|
||||
}
|
||||
|
||||
function formatResponse(response) {
|
||||
function formatSubmitResponse(response) {
|
||||
const data = {
|
||||
resultCode: response.engine_result,
|
||||
resultMessage: response.engine_result_message
|
||||
@@ -36,11 +28,12 @@ function formatResponse(response) {
|
||||
|
||||
function submit(signedTransaction: string): Promise<Submit> {
|
||||
validate.submit({signedTransaction});
|
||||
|
||||
const request = {
|
||||
command: 'submit',
|
||||
tx_blob: signedTransaction
|
||||
};
|
||||
return this.connection.request(request).then(formatResponse);
|
||||
return this.connection.request(request).then(formatSubmitResponse);
|
||||
}
|
||||
|
||||
module.exports = submit;
|
||||
|
||||
@@ -17,3 +17,12 @@ export type Prepare = {
|
||||
maxLedgerVersion?: number
|
||||
}
|
||||
}
|
||||
|
||||
export type Submit = {
|
||||
success: boolean,
|
||||
engineResult: string,
|
||||
engineResultCode: number,
|
||||
engineResultMessage?: string,
|
||||
txBlob?: string,
|
||||
txJson?: Object
|
||||
}
|
||||
|
||||
@@ -27,6 +27,10 @@ function setCanonicalFlag(txJSON) {
|
||||
txJSON.Flags = txJSON.Flags >>> 0;
|
||||
}
|
||||
|
||||
function scaleValue(value, multiplier) {
|
||||
return (new BigNumber(value)).times(multiplier).toString();
|
||||
}
|
||||
|
||||
function prepareTransaction(txJSON: Object, api: Object,
|
||||
instructions: Instructions
|
||||
): Promise<Prepare> {
|
||||
@@ -51,8 +55,9 @@ function prepareTransaction(txJSON: Object, api: Object,
|
||||
}
|
||||
|
||||
function prepareFee(): Promise<Object> {
|
||||
const multiplier = (txJSON.Signers || []).length + 1;
|
||||
if (instructions.fee !== undefined) {
|
||||
txJSON.Fee = common.xrpToDrops(instructions.fee);
|
||||
txJSON.Fee = scaleValue(common.xrpToDrops(instructions.fee), multiplier);
|
||||
return Promise.resolve(txJSON);
|
||||
}
|
||||
const cushion = api._feeCushion;
|
||||
@@ -60,9 +65,10 @@ function prepareTransaction(txJSON: Object, api: Object,
|
||||
const feeDrops = common.xrpToDrops(fee);
|
||||
if (instructions.maxFee !== undefined) {
|
||||
const maxFeeDrops = common.xrpToDrops(instructions.maxFee);
|
||||
txJSON.Fee = BigNumber.min(feeDrops, maxFeeDrops).toString();
|
||||
const normalFee = BigNumber.min(feeDrops, maxFeeDrops).toString();
|
||||
txJSON.Fee = scaleValue(normalFee, multiplier);
|
||||
} else {
|
||||
txJSON.Fee = feeDrops;
|
||||
txJSON.Fee = scaleValue(feeDrops, multiplier);
|
||||
}
|
||||
return txJSON;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user