@nextrope/xrpl
Version:
A TypeScript/JavaScript API for interacting with the XRP Ledger in Node.js and the browser
199 lines (180 loc) • 6.95 kB
text/typescript
import { encode, encodeForSigningBatch } from 'ripple-binary-codec'
import { sign } from 'ripple-keypairs'
import { ValidationError } from '../errors'
import { Batch, Transaction, validate } from '../models'
import { BatchSigner, validateBatch } from '../models/transactions/batch'
import { hashSignedTx } from '../utils/hashes'
import { compareSigners, getDecodedTransaction } from './utils'
import { Wallet } from '.'
// eslint-disable-next-line max-params -- okay for helper function
function constructBatchSignerObject(
batchAccount: string,
wallet: Wallet,
signature: string,
multisignAddress: string | false = false,
): BatchSigner {
let batchSigner: BatchSigner
if (multisignAddress) {
batchSigner = {
BatchSigner: {
Account: batchAccount,
Signers: [
{
Signer: {
Account: multisignAddress,
SigningPubKey: wallet.publicKey,
TxnSignature: signature,
},
},
],
},
}
} else {
batchSigner = {
BatchSigner: {
Account: batchAccount,
SigningPubKey: wallet.publicKey,
TxnSignature: signature,
},
}
}
return batchSigner
}
/**
* Sign a multi-account Batch transaction.
*
* @param wallet - Wallet instance.
* @param transaction - The Batch transaction to sign.
* @param opts - Additional options for regular key and multi-signing complexity.
* @param opts.batchAccount - The account submitting the inner Batch transaction, on behalf of which is this signature.
* @param opts.multisign - Specify true/false to use multisign or actual address (classic/x-address) to make multisign tx request.
* The actual address is only needed in the case of regular key usage.
* @throws ValidationError if the transaction is malformed.
*/
export function signMultiBatch(
wallet: Wallet,
transaction: Batch,
opts: { batchAccount?: string; multisign?: boolean | string } = {},
): void {
const batchAccount = opts.batchAccount ?? wallet.classicAddress
let multisignAddress: boolean | string = false
if (typeof opts.multisign === 'string') {
multisignAddress = opts.multisign
} else if (opts.multisign) {
multisignAddress = wallet.classicAddress
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- for JS purposes
if (transaction.TransactionType !== 'Batch') {
throw new ValidationError('Must be a Batch transaction.')
}
/*
* This will throw a more clear error for JS users if the supplied transaction has incorrect formatting
*/
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- validate does not accept Transaction type
validate(transaction as unknown as Record<string, unknown>)
const involvedAccounts = new Set(
transaction.RawTransactions.map((raw) => raw.RawTransaction.Account),
)
if (!involvedAccounts.has(batchAccount)) {
throw new ValidationError(
'Must be signing for an address submitting a transaction in the Batch.',
)
}
const fieldsToSign = {
flags: transaction.Flags,
txIDs: transaction.RawTransactions.map((rawTx) =>
hashSignedTx(rawTx.RawTransaction),
),
}
const signature = sign(encodeForSigningBatch(fieldsToSign), wallet.privateKey)
// eslint-disable-next-line no-param-reassign -- okay for signing
transaction.BatchSigners = [
constructBatchSignerObject(
batchAccount,
wallet,
signature,
multisignAddress,
),
]
}
/**
* Takes several transactions with BatchSigners fields (in object or blob form) and creates a
* single transaction with all BatchSigners that then gets signed and returned.
*
* @param transactions The transactions to combine `BatchSigners` values on.
* @returns A single signed Transaction which has all BatchSigners from transactions within it.
* @throws ValidationError if:
* - There were no transactions given to sign
* @category Signing
*/
export function combineBatchSigners(
transactions: Array<Batch | string>,
): string {
if (transactions.length === 0) {
throw new ValidationError('There are 0 transactions to combine.')
}
const decodedTransactions: Transaction[] = transactions.map((txOrBlob) => {
return getDecodedTransaction(txOrBlob)
})
decodedTransactions.forEach((tx) => {
if (tx.TransactionType !== 'Batch') {
throw new ValidationError('TransactionType must be `Batch`.')
}
/*
* This will throw a more clear error for JS users if any of the supplied transactions has incorrect formatting
*/
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- validate does not accept Transaction type
validateBatch(tx as unknown as Record<string, unknown>)
if (tx.BatchSigners == null || tx.BatchSigners.length === 0) {
throw new ValidationError(
'For combining Batch transaction signatures, all transactions must include a BatchSigners field containing an array of signatures.',
)
}
if (tx.TxnSignature != null || tx.Signers != null) {
throw new ValidationError('Batch transaction must be unsigned.')
}
})
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- checked above
const batchTransactions = decodedTransactions as Batch[]
validateBatchTransactionEquivalence(batchTransactions)
return encode(getTransactionWithAllBatchSigners(batchTransactions))
}
/**
* The transactions should all be equal except for the 'Signers' field.
*
* @param transactions - An array of Transactions which are expected to be equal other than 'Signers'.
* @throws ValidationError if the transactions are not equal in any field other than 'Signers'.
*/
function validateBatchTransactionEquivalence(transactions: Batch[]): void {
const exampleTransaction = JSON.stringify({
flags: transactions[0].Flags,
transactionIDs: transactions[0].RawTransactions.map((rawTx) =>
hashSignedTx(rawTx.RawTransaction),
),
})
if (
transactions.slice(1).some(
(tx) =>
JSON.stringify({
flags: tx.Flags,
transactionIDs: tx.RawTransactions.map((rawTx) =>
hashSignedTx(rawTx.RawTransaction),
),
}) !== exampleTransaction,
)
) {
throw new ValidationError(
'Flags and transaction hashes are not the same for all provided transactions.',
)
}
}
function getTransactionWithAllBatchSigners(transactions: Batch[]): Batch {
// Signers must be sorted in the combined transaction - See compareSigners' documentation for more details
const sortedSigners: BatchSigner[] = transactions
.flatMap((tx) => tx.BatchSigners ?? [])
.filter((signer) => signer.BatchSigner.Account !== transactions[0].Account)
.sort((signer1, signer2) =>
compareSigners(signer1.BatchSigner, signer2.BatchSigner),
)
return { ...transactions[0], BatchSigners: sortedSigners }
}