mirror of
https://github.com/Xahau/xahau.js.git
synced 2025-11-20 12:15:51 +00:00
Signer (#1573)
Create Signer class to allow for offline signing, multisigning and signing authorizeChannel requests.
This commit is contained in:
committed by
Mayukha Vadari
parent
04dd65af38
commit
c401f703c7
@@ -1,3 +1,5 @@
|
|||||||
|
import { Signer } from '../../../models/common'
|
||||||
|
|
||||||
import { RippledAmount } from './amounts'
|
import { RippledAmount } from './amounts'
|
||||||
import { Memo } from './memos'
|
import { Memo } from './memos'
|
||||||
|
|
||||||
@@ -10,7 +12,7 @@ export interface OfferCreateTransaction {
|
|||||||
Flags: number
|
Flags: number
|
||||||
LastLedgerSequence?: number
|
LastLedgerSequence?: number
|
||||||
Sequence: number
|
Sequence: number
|
||||||
Signers: any[]
|
Signers: Signer[]
|
||||||
SigningPubKey: string
|
SigningPubKey: string
|
||||||
SourceTag?: number
|
SourceTag?: number
|
||||||
TakerGets: RippledAmount
|
TakerGets: RippledAmount
|
||||||
|
|||||||
@@ -26,10 +26,12 @@ export interface IssuedCurrencyAmount extends IssuedCurrency {
|
|||||||
export type Amount = IssuedCurrencyAmount | string
|
export type Amount = IssuedCurrencyAmount | string
|
||||||
|
|
||||||
export interface Signer {
|
export interface Signer {
|
||||||
|
Signer: {
|
||||||
Account: string
|
Account: string
|
||||||
TxnSignature: string
|
TxnSignature: string
|
||||||
SigningPubKey: string
|
SigningPubKey: string
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface Memo {
|
export interface Memo {
|
||||||
MemoData?: string
|
MemoData?: string
|
||||||
|
|||||||
@@ -55,7 +55,13 @@ const SIGNER_SIZE = 3
|
|||||||
|
|
||||||
function isSigner(obj: unknown): boolean {
|
function isSigner(obj: unknown): boolean {
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Only used by JS
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Only used by JS
|
||||||
const signer = obj as Record<string, unknown>
|
const signerWrapper = obj as Record<string, unknown>
|
||||||
|
|
||||||
|
if (signerWrapper.Signer == null) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Only used by JS and Signer is previously unknown
|
||||||
|
const signer = signerWrapper.Signer as Record<string, unknown>
|
||||||
return (
|
return (
|
||||||
Object.keys(signer).length === SIGNER_SIZE &&
|
Object.keys(signer).length === SIGNER_SIZE &&
|
||||||
typeof signer.Account === 'string' &&
|
typeof signer.Account === 'string' &&
|
||||||
@@ -105,6 +111,7 @@ export interface BaseTransaction {
|
|||||||
AccountTxnID?: string
|
AccountTxnID?: string
|
||||||
Flags?: number | GlobalFlags
|
Flags?: number | GlobalFlags
|
||||||
LastLedgerSequence?: number
|
LastLedgerSequence?: number
|
||||||
|
// TODO: Make Memo match the format of Signer (By including the Memo: wrapper inside the Interface)
|
||||||
Memos?: Array<{ Memo: Memo }>
|
Memos?: Array<{ Memo: Memo }>
|
||||||
Signers?: Signer[]
|
Signers?: Signer[]
|
||||||
SourceTag?: number
|
SourceTag?: number
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import keypairs from 'ripple-keypairs'
|
|||||||
|
|
||||||
import type { Client, Wallet } from '..'
|
import type { Client, Wallet } from '..'
|
||||||
import { ValidationError } from '../common/errors'
|
import { ValidationError } from '../common/errors'
|
||||||
import { SignedTransaction } from '../common/types/objects'
|
|
||||||
import { xrpToDrops } from '../utils'
|
import { xrpToDrops } from '../utils'
|
||||||
import { computeBinaryTransactionHash } from '../utils/hashes'
|
import { computeBinaryTransactionHash } from '../utils/hashes'
|
||||||
|
|
||||||
@@ -25,7 +24,7 @@ function signWithKeypair(
|
|||||||
options: SignOptions = {
|
options: SignOptions = {
|
||||||
signAs: '',
|
signAs: '',
|
||||||
},
|
},
|
||||||
): SignedTransaction {
|
): { signedTransaction: string; id: string } {
|
||||||
const tx = JSON.parse(txJSON)
|
const tx = JSON.parse(txJSON)
|
||||||
if (tx.TxnSignature || tx.Signers) {
|
if (tx.TxnSignature || tx.Signers) {
|
||||||
throw new ValidationError(
|
throw new ValidationError(
|
||||||
@@ -222,7 +221,7 @@ function sign(
|
|||||||
secret?: any,
|
secret?: any,
|
||||||
options?: SignOptions,
|
options?: SignOptions,
|
||||||
keypair?: KeyPair,
|
keypair?: KeyPair,
|
||||||
): SignedTransaction {
|
): { signedTransaction: string; id: string } {
|
||||||
if (typeof secret === 'string') {
|
if (typeof secret === 'string') {
|
||||||
// we can't validate that the secret matches the account because
|
// we can't validate that the secret matches the account because
|
||||||
// the secret could correspond to the regular key
|
// the secret could correspond to the regular key
|
||||||
@@ -245,7 +244,7 @@ function signOffline(
|
|||||||
wallet: Wallet,
|
wallet: Wallet,
|
||||||
txJSON: string,
|
txJSON: string,
|
||||||
options?: SignOptions,
|
options?: SignOptions,
|
||||||
): SignedTransaction {
|
): { signedTransaction: string; id: string } {
|
||||||
const { publicKey, privateKey } = wallet
|
const { publicKey, privateKey } = wallet
|
||||||
return signWithKeypair(null, txJSON, { publicKey, privateKey }, options)
|
return signWithKeypair(null, txJSON, { publicKey, privateKey }, options)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
|
|
||||||
import ECDSA from '../common/ecdsa'
|
import ECDSA from '../common/ecdsa'
|
||||||
import { ValidationError } from '../common/errors'
|
import { ValidationError } from '../common/errors'
|
||||||
import { SignedTransaction } from '../common/types/objects'
|
|
||||||
import { signOffline } from '../transaction/sign'
|
import { signOffline } from '../transaction/sign'
|
||||||
import { SignOptions } from '../transaction/types'
|
import { SignOptions } from '../transaction/types'
|
||||||
|
|
||||||
@@ -126,8 +125,9 @@ class Wallet {
|
|||||||
signTransaction(
|
signTransaction(
|
||||||
transaction: any, // TODO: transaction should be typed with Transaction type.
|
transaction: any, // TODO: transaction should be typed with Transaction type.
|
||||||
options: SignOptions = { signAs: '' },
|
options: SignOptions = { signAs: '' },
|
||||||
): SignedTransaction {
|
): string {
|
||||||
return signOffline(this, JSON.stringify(transaction), options)
|
return signOffline(this, JSON.stringify(transaction), options)
|
||||||
|
.signedTransaction
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -153,6 +153,10 @@ class Wallet {
|
|||||||
getXAddress(tag: number, test = false): string {
|
getXAddress(tag: number, test = false): string {
|
||||||
return classicAddressToXAddress(this.classicAddress, tag, test)
|
return classicAddressToXAddress(this.classicAddress, tag, test)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getClassicAddress(): string {
|
||||||
|
return deriveAddress(this.publicKey)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Wallet
|
export default Wallet
|
||||||
|
|||||||
185
src/wallet/signer.ts
Normal file
185
src/wallet/signer.ts
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
import { BigNumber } from 'bignumber.js'
|
||||||
|
import { flatMap } from 'lodash'
|
||||||
|
import { decodeAccountID } from 'ripple-address-codec'
|
||||||
|
import {
|
||||||
|
decode,
|
||||||
|
encodeForSigning,
|
||||||
|
encodeForSigningClaim,
|
||||||
|
} from 'ripple-binary-codec'
|
||||||
|
import { sign as signWithKeypair, verify } from 'ripple-keypairs'
|
||||||
|
|
||||||
|
import { ValidationError } from '../common/errors'
|
||||||
|
import { Signer } from '../models/common'
|
||||||
|
import { Transaction } from '../models/transactions'
|
||||||
|
import { verifyBaseTransaction } from '../models/transactions/common'
|
||||||
|
|
||||||
|
import Wallet from '.'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uses a wallet to cryptographically sign a transaction which proves the owner of the wallet
|
||||||
|
* is issuing this transaction.
|
||||||
|
*
|
||||||
|
* @param wallet - A Wallet that holds your cryptographic keys.
|
||||||
|
* @param tx - The Transaction that is being signed.
|
||||||
|
* @param forMultisign - If true, changes the signature format to encode for multisigning.
|
||||||
|
* @returns A signed Transaction.
|
||||||
|
*/
|
||||||
|
function sign(wallet: Wallet, tx: Transaction, forMultisign = false): string {
|
||||||
|
return wallet.signTransaction(
|
||||||
|
tx,
|
||||||
|
forMultisign ? { signAs: wallet.getClassicAddress() } : { signAs: '' },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes several transactions (in object or blob form) and creates a single transaction with all Signers
|
||||||
|
* that then gets signed and returned.
|
||||||
|
*
|
||||||
|
* @param transactions - An array of Transactions (in object or blob form) to combine and sign.
|
||||||
|
* @returns A single signed Transaction which has all Signers from transactions within it.
|
||||||
|
* @throws ValidationError if:
|
||||||
|
* - There were no transactions given to sign
|
||||||
|
* - The SigningPubKey field is not the empty string in any given transaction
|
||||||
|
* - Any transaction is missing a Signers field.
|
||||||
|
*/
|
||||||
|
function multisign(transactions: Array<Transaction | string>): Transaction {
|
||||||
|
if (transactions.length === 0) {
|
||||||
|
throw new ValidationError('There were 0 transactions to multisign')
|
||||||
|
}
|
||||||
|
|
||||||
|
transactions.forEach((txOrBlob) => {
|
||||||
|
const tx: Transaction = getDecodedTransaction(txOrBlob)
|
||||||
|
|
||||||
|
// This will throw a more clear error for JS users if any of the supplied transactions has incorrect formatting
|
||||||
|
// TODO: Replace this with verify() (The general validation function for all Transactions)
|
||||||
|
// , also make verify accept '| Transaction' to avoid type casting here.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- verify does not accept Transaction type
|
||||||
|
verifyBaseTransaction(tx as unknown as Record<string, unknown>)
|
||||||
|
if (tx.Signers == null || tx.Signers.length === 0) {
|
||||||
|
throw new ValidationError(
|
||||||
|
"For multisigning all transactions must include a Signers field containing an array of signatures. You may have forgotten to pass the 'forMultisign' parameter when signing.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tx.SigningPubKey !== '') {
|
||||||
|
throw new ValidationError(
|
||||||
|
'SigningPubKey must be an empty string for all transactions when multisigning.',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const decodedTransactions: Transaction[] = transactions.map(
|
||||||
|
(txOrBlob: string | Transaction) => {
|
||||||
|
return getDecodedTransaction(txOrBlob)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
validateTransactionEquivalence(decodedTransactions)
|
||||||
|
|
||||||
|
return getTransactionWithAllSigners(decodedTransactions)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a signature that can be used to redeem a specific amount of XRP from a payment channel.
|
||||||
|
*
|
||||||
|
* @param wallet - The account that will sign for this payment channel.
|
||||||
|
* @param channelId - An id for the payment channel to redeem XRP from.
|
||||||
|
* @param amount - The amount in drops to redeem.
|
||||||
|
* @returns A signature that can be used to redeem a specific amount of XRP from a payment channel.
|
||||||
|
*/
|
||||||
|
function authorizeChannel(
|
||||||
|
wallet: Wallet,
|
||||||
|
channelId: string,
|
||||||
|
amount: string,
|
||||||
|
): string {
|
||||||
|
const signingData = encodeForSigningClaim({
|
||||||
|
channel: channelId,
|
||||||
|
amount,
|
||||||
|
})
|
||||||
|
|
||||||
|
return signWithKeypair(signingData, wallet.privateKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies that the given transaction has a valid signature based on public-key encryption.
|
||||||
|
*
|
||||||
|
* @param tx - A transaction to verify the signature of. (Can be in object or encoded string format).
|
||||||
|
* @returns Returns true if tx has a valid signature, and returns false otherwise.
|
||||||
|
*/
|
||||||
|
function verifySignature(tx: Transaction | string): boolean {
|
||||||
|
const decodedTx: Transaction = getDecodedTransaction(tx)
|
||||||
|
return verify(
|
||||||
|
encodeForSigning(decodedTx),
|
||||||
|
decodedTx.TxnSignature,
|
||||||
|
decodedTx.SigningPubKey,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 validateTransactionEquivalence(transactions: Transaction[]): void {
|
||||||
|
const exampleTransaction = JSON.stringify({
|
||||||
|
...transactions[0],
|
||||||
|
Signers: null,
|
||||||
|
})
|
||||||
|
if (
|
||||||
|
transactions
|
||||||
|
.slice(1)
|
||||||
|
.some(
|
||||||
|
(tx) => JSON.stringify({ ...tx, Signers: null }) !== exampleTransaction,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new ValidationError(
|
||||||
|
'txJSON is not the same for all signedTransactions',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTransactionWithAllSigners(
|
||||||
|
transactions: Transaction[],
|
||||||
|
): Transaction {
|
||||||
|
// Signers must be sorted in the combined transaction - See compareSigners' documentation for more details
|
||||||
|
const sortedSigners: Signer[] = flatMap(
|
||||||
|
transactions,
|
||||||
|
(tx) => tx.Signers ?? [],
|
||||||
|
).sort(compareSigners)
|
||||||
|
|
||||||
|
return { ...transactions[0], Signers: sortedSigners }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If presented in binary form, the Signers array must be sorted based on
|
||||||
|
* the numeric value of the signer addresses, with the lowest value first.
|
||||||
|
* (If submitted as JSON, the submit_multisigned method handles this automatically.)
|
||||||
|
* https://xrpl.org/multi-signing.html.
|
||||||
|
*
|
||||||
|
* @param left - A Signer to compare with.
|
||||||
|
* @param right - A second Signer to compare with.
|
||||||
|
* @returns 1 if left \> right, 0 if left = right, -1 if left \< right, and null if left or right are NaN.
|
||||||
|
*/
|
||||||
|
function compareSigners(left: Signer, right: Signer): number {
|
||||||
|
return addressToBigNumber(left.Signer.Account).comparedTo(
|
||||||
|
addressToBigNumber(right.Signer.Account),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function addressToBigNumber(address: string): BigNumber {
|
||||||
|
const hex = Buffer.from(decodeAccountID(address)).toString('hex')
|
||||||
|
const numberOfBitsInHex = 16
|
||||||
|
return new BigNumber(hex, numberOfBitsInHex)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDecodedTransaction(txOrBlob: Transaction | string): Transaction {
|
||||||
|
if (typeof txOrBlob === 'object') {
|
||||||
|
return txOrBlob
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- We are casting here to get strong typing
|
||||||
|
return decode(txOrBlob) as unknown as Transaction
|
||||||
|
}
|
||||||
|
|
||||||
|
export { sign, authorizeChannel, verifySignature, multisign }
|
||||||
@@ -42,10 +42,12 @@ describe('BaseTransaction', function () {
|
|||||||
],
|
],
|
||||||
Signers: [
|
Signers: [
|
||||||
{
|
{
|
||||||
|
Signer: {
|
||||||
Account: 'r....',
|
Account: 'r....',
|
||||||
TxnSignature: 'DEADBEEF',
|
TxnSignature: 'DEADBEEF',
|
||||||
SigningPubKey: 'hex-string',
|
SigningPubKey: 'hex-string',
|
||||||
},
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
SourceTag: 31,
|
SourceTag: 31,
|
||||||
SigningPublicKey:
|
SigningPublicKey:
|
||||||
@@ -197,8 +199,10 @@ describe('BaseTransaction', function () {
|
|||||||
TransactionType: 'Payment',
|
TransactionType: 'Payment',
|
||||||
Signers: [
|
Signers: [
|
||||||
{
|
{
|
||||||
|
Signer: {
|
||||||
Account: 'r....',
|
Account: 'r....',
|
||||||
},
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
} as any
|
} as any
|
||||||
|
|
||||||
|
|||||||
@@ -165,12 +165,10 @@ describe('Wallet', function () {
|
|||||||
SigningPubKey: publicKey,
|
SigningPubKey: publicKey,
|
||||||
}
|
}
|
||||||
const wallet = new Wallet(publicKey, privateKey)
|
const wallet = new Wallet(publicKey, privateKey)
|
||||||
const signedTx: { signedTransaction: string; id: string } =
|
const signedTx: string = wallet.signTransaction(txJSON)
|
||||||
wallet.signTransaction(txJSON)
|
|
||||||
|
|
||||||
assert.hasAllKeys(signedTx, ['id', 'signedTransaction'])
|
// TODO: Check the output of the signature against a known result
|
||||||
assert.isString(signedTx.id)
|
assert.isString(signedTx)
|
||||||
assert.isString(signedTx.signedTransaction)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
303
test/wallet/signer.ts
Normal file
303
test/wallet/signer.ts
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
import { assert } from 'chai'
|
||||||
|
import { decode, encode } from 'ripple-binary-codec/dist'
|
||||||
|
import { JsonObject } from 'ripple-binary-codec/dist/types/serialized-type'
|
||||||
|
|
||||||
|
import { ValidationError } from '../../src/common/errors'
|
||||||
|
import { Transaction } from '../../src/models/transactions'
|
||||||
|
import Wallet from '../../src/wallet'
|
||||||
|
import {
|
||||||
|
sign,
|
||||||
|
authorizeChannel,
|
||||||
|
multisign,
|
||||||
|
verifySignature,
|
||||||
|
} from '../../src/wallet/signer'
|
||||||
|
|
||||||
|
const publicKey =
|
||||||
|
'030E58CDD076E798C84755590AAF6237CA8FAE821070A59F648B517A30DC6F589D'
|
||||||
|
const privateKey =
|
||||||
|
'00141BA006D3363D2FB2785E8DF4E44D3A49908780CB4FB51F6D217C08C021429F'
|
||||||
|
const address = 'rhvh5SrgBL5V8oeV9EpDuVszeJSSCEkbPc'
|
||||||
|
const seed = 'ss1x3KLrSvfg7irFc1D929WXZ7z9H'
|
||||||
|
|
||||||
|
const tx: Transaction = {
|
||||||
|
TransactionType: 'Payment',
|
||||||
|
Account: address,
|
||||||
|
Destination: 'rQ3PTWGLCbPz8ZCicV5tCX3xuymojTng5r',
|
||||||
|
Amount: '20000000',
|
||||||
|
Sequence: 1,
|
||||||
|
Fee: '12',
|
||||||
|
SigningPubKey: publicKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsignedTx1: Transaction = {
|
||||||
|
TransactionType: 'TrustSet',
|
||||||
|
Account: 'rEuLyBCvcw4CFmzv8RepSiAoNgF8tTGJQC',
|
||||||
|
Fee: '30000',
|
||||||
|
Flags: 262144,
|
||||||
|
LimitAmount: {
|
||||||
|
currency: 'USD',
|
||||||
|
issuer: 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh',
|
||||||
|
value: '100',
|
||||||
|
},
|
||||||
|
Sequence: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsignedSecret1 = 'spzGHmohX9bAM6gzF4m9FvJmJb1CR'
|
||||||
|
|
||||||
|
const multisignTx1: Transaction = {
|
||||||
|
TransactionType: 'TrustSet',
|
||||||
|
Account: 'rEuLyBCvcw4CFmzv8RepSiAoNgF8tTGJQC',
|
||||||
|
Fee: '30000',
|
||||||
|
Flags: 262144,
|
||||||
|
LimitAmount: {
|
||||||
|
currency: 'USD',
|
||||||
|
issuer: 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh',
|
||||||
|
value: '100',
|
||||||
|
},
|
||||||
|
Sequence: 2,
|
||||||
|
Signers: [
|
||||||
|
{
|
||||||
|
Signer: {
|
||||||
|
Account: 'rJvuSQhQR37czfxRou4vNWaM97uEhT4ShE',
|
||||||
|
SigningPubKey:
|
||||||
|
'02B78EEA571B2633180834CC6E7B4ED84FBF6811D12ECB59410E0C92D13B7726F5',
|
||||||
|
TxnSignature:
|
||||||
|
'304502210098009CEFA61EE9843BB7FC29B78CFFAACF28352A4A7CF3AAE79EF12D79BA50910220684F116266E5E4519A7A33F7421631EB8494082BE51A8B03FECCB3E59F77154A',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
SigningPubKey: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
const multisignTxToCombine1: Transaction = {
|
||||||
|
Account: 'rEuLyBCvcw4CFmzv8RepSiAoNgF8tTGJQC',
|
||||||
|
Fee: '30000',
|
||||||
|
Flags: 262144,
|
||||||
|
LimitAmount: {
|
||||||
|
currency: 'USD',
|
||||||
|
issuer: 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh',
|
||||||
|
value: '100',
|
||||||
|
},
|
||||||
|
Sequence: 2,
|
||||||
|
Signers: [
|
||||||
|
{
|
||||||
|
Signer: {
|
||||||
|
Account: 'rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW',
|
||||||
|
SigningPubKey:
|
||||||
|
'02B3EC4E5DD96029A647CFA20DA07FE1F85296505552CCAC114087E66B46BD77DF',
|
||||||
|
TxnSignature:
|
||||||
|
'30450221009C195DBBF7967E223D8626CA19CF02073667F2B22E206727BFE848FF42BEAC8A022048C323B0BED19A988BDBEFA974B6DE8AA9DCAE250AA82BBD1221787032A864E5',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
SigningPubKey: '',
|
||||||
|
TransactionType: 'TrustSet',
|
||||||
|
}
|
||||||
|
|
||||||
|
const multisignTxToCombine2: Transaction = {
|
||||||
|
Account: 'rEuLyBCvcw4CFmzv8RepSiAoNgF8tTGJQC',
|
||||||
|
Fee: '30000',
|
||||||
|
Flags: 262144,
|
||||||
|
LimitAmount: {
|
||||||
|
currency: 'USD',
|
||||||
|
issuer: 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh',
|
||||||
|
value: '100',
|
||||||
|
},
|
||||||
|
Sequence: 2,
|
||||||
|
Signers: [
|
||||||
|
{
|
||||||
|
Signer: {
|
||||||
|
Account: 'rJvuSQhQR37czfxRou4vNWaM97uEhT4ShE',
|
||||||
|
SigningPubKey:
|
||||||
|
'02B78EEA571B2633180834CC6E7B4ED84FBF6811D12ECB59410E0C92D13B7726F5',
|
||||||
|
TxnSignature:
|
||||||
|
'304502210098009CEFA61EE9843BB7FC29B78CFFAACF28352A4A7CF3AAE79EF12D79BA50910220684F116266E5E4519A7A33F7421631EB8494082BE51A8B03FECCB3E59F77154A',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
SigningPubKey: '',
|
||||||
|
TransactionType: 'TrustSet',
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedMultisign: Transaction = {
|
||||||
|
Account: 'rEuLyBCvcw4CFmzv8RepSiAoNgF8tTGJQC',
|
||||||
|
Fee: '30000',
|
||||||
|
Flags: 262144,
|
||||||
|
LimitAmount: {
|
||||||
|
currency: 'USD',
|
||||||
|
issuer: 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh',
|
||||||
|
value: '100',
|
||||||
|
},
|
||||||
|
Sequence: 2,
|
||||||
|
Signers: [
|
||||||
|
{
|
||||||
|
Signer: {
|
||||||
|
Account: 'rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW',
|
||||||
|
SigningPubKey:
|
||||||
|
'02B3EC4E5DD96029A647CFA20DA07FE1F85296505552CCAC114087E66B46BD77DF',
|
||||||
|
TxnSignature:
|
||||||
|
'30450221009C195DBBF7967E223D8626CA19CF02073667F2B22E206727BFE848FF42BEAC8A022048C323B0BED19A988BDBEFA974B6DE8AA9DCAE250AA82BBD1221787032A864E5',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Signer: {
|
||||||
|
Account: 'rJvuSQhQR37czfxRou4vNWaM97uEhT4ShE',
|
||||||
|
SigningPubKey:
|
||||||
|
'02B78EEA571B2633180834CC6E7B4ED84FBF6811D12ECB59410E0C92D13B7726F5',
|
||||||
|
TxnSignature:
|
||||||
|
'304502210098009CEFA61EE9843BB7FC29B78CFFAACF28352A4A7CF3AAE79EF12D79BA50910220684F116266E5E4519A7A33F7421631EB8494082BE51A8B03FECCB3E59F77154A',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
SigningPubKey: '',
|
||||||
|
TransactionType: 'TrustSet',
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Signer', function () {
|
||||||
|
it('sign', function () {
|
||||||
|
// Test case data generated using this tutorial - https://xrpl.org/send-xrp.html#send-xrp
|
||||||
|
const tx3: Transaction = {
|
||||||
|
TransactionType: 'Payment',
|
||||||
|
Account: 'rHLEki8gPUMnF72JnuALvnAMRhRemzhRke',
|
||||||
|
Amount: '22000000',
|
||||||
|
Destination: 'rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe',
|
||||||
|
Flags: 2147483648,
|
||||||
|
LastLedgerSequence: 20582339,
|
||||||
|
Fee: '12',
|
||||||
|
Sequence: 20582260,
|
||||||
|
}
|
||||||
|
const signedTxBlob =
|
||||||
|
'120000228000000024013A0F74201B013A0FC36140000000014FB18068400000000000000C732102A8A44DB3D4C73EEEE11DFE54D2029103B776AA8A8D293A91D645977C9DF5F544744730450221009ECB5324717E14DD6970126271F05BC2626D2A8FA9F3797555D417F8257C1E6002206BDD74A0F30425F2BA9DB69C90F21B3E27735C190FB4F3A640F066ACBBF06AD98114B3263BD0A9BF9DFDBBBBD07F536355FF477BF0E98314F667B0CA50CC7709A220B0561B85E53A48461FA8'
|
||||||
|
|
||||||
|
const wallet = Wallet.fromSeed(seed)
|
||||||
|
|
||||||
|
const signedTx: string = sign(wallet, tx3)
|
||||||
|
|
||||||
|
assert.equal(signedTx, signedTxBlob)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sign in multisign format', function () {
|
||||||
|
const wallet = Wallet.fromSeed(unsignedSecret1)
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
decode(sign(wallet, unsignedTx1, true)),
|
||||||
|
multisignTx1 as unknown as JsonObject,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('multisign runs successfully with Transaction objects', function () {
|
||||||
|
const transactions: Transaction[] = [
|
||||||
|
multisignTxToCombine1,
|
||||||
|
multisignTxToCombine2,
|
||||||
|
]
|
||||||
|
|
||||||
|
assert.deepEqual(multisign(transactions), expectedMultisign)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('multisign runs successfully with tx_blobs', function () {
|
||||||
|
const transactions: Transaction[] = [
|
||||||
|
multisignTxToCombine1,
|
||||||
|
multisignTxToCombine2,
|
||||||
|
]
|
||||||
|
|
||||||
|
const encodedTransactions: string[] = transactions.map(encode)
|
||||||
|
|
||||||
|
assert.deepEqual(multisign(encodedTransactions), expectedMultisign)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('multisign throws a validation error when there are no transactions', function () {
|
||||||
|
const transactions: Transaction[] = []
|
||||||
|
assert.throws(() => multisign(transactions), ValidationError)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('multisign throws when trying to combine two different transactions', function () {
|
||||||
|
const differentMultisignedTx: Transaction = {
|
||||||
|
TransactionType: 'Payment',
|
||||||
|
Sequence: 1,
|
||||||
|
Amount: '20000000',
|
||||||
|
Fee: '12',
|
||||||
|
SigningPubKey: '',
|
||||||
|
Account: 'rhvh5SrgBL5V8oeV9EpDuVszeJSSCEkbPc',
|
||||||
|
Destination: 'rQ3PTWGLCbPz8ZCicV5tCX3xuymojTng5r',
|
||||||
|
Signers: [
|
||||||
|
{
|
||||||
|
Signer: {
|
||||||
|
SigningPubKey:
|
||||||
|
'02A8A44DB3D4C73EEEE11DFE54D2029103B776AA8A8D293A91D645977C9DF5F544',
|
||||||
|
TxnSignature:
|
||||||
|
'3044022077BCE143B9A0B51A7716BB93CBC0C99FB41BA339D91A87CB9E47DA80A7EF660802205C81AA49D408771F65A131200CCBFC536ACFE212C1414E05E43B56BE1F9380F2',
|
||||||
|
Account: 'rHLEki8gPUMnF72JnuALvnAMRhRemzhRke',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const transactions: Transaction[] = [
|
||||||
|
multisignTxToCombine1,
|
||||||
|
differentMultisignedTx,
|
||||||
|
]
|
||||||
|
|
||||||
|
assert.throws(() => multisign(transactions))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('multisign throws when trying to combine transaction with normal signature', function () {
|
||||||
|
const signedTxBlob =
|
||||||
|
'120000228000000024013A0F74201B013A0FC36140000000014FB18068400000000000000C732102A8A44DB3D4C73EEEE11DFE54D2029103B776AA8A8D293A91D645977C9DF5F544744730450221009ECB5324717E14DD6970126271F05BC2626D2A8FA9F3797555D417F8257C1E6002206BDD74A0F30425F2BA9DB69C90F21B3E27735C190FB4F3A640F066ACBBF06AD98114B3263BD0A9BF9DFDBBBBD07F536355FF477BF0E98314F667B0CA50CC7709A220B0561B85E53A48461FA8'
|
||||||
|
|
||||||
|
const transactions = [signedTxBlob]
|
||||||
|
|
||||||
|
assert.throws(() => multisign(transactions), /forMultisign/u)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('authorizeChannel succeeds with secp256k1 seed', function () {
|
||||||
|
const wallet = Wallet.fromSeed('snGHNrPbHrdUcszeuDEigMdC1Lyyd')
|
||||||
|
const channelId =
|
||||||
|
'5DB01B7FFED6B67E6B0414DED11E051D2EE2B7619CE0EAA6286D67A3A4D5BDB3'
|
||||||
|
const amount = '1000000'
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
authorizeChannel(wallet, channelId, amount),
|
||||||
|
'304402204E7052F33DDAFAAA55C9F5B132A5E50EE95B2CF68C0902F61DFE77299BC893740220353640B951DCD24371C16868B3F91B78D38B6F3FD1E826413CDF891FA8250AAC',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('authorizeChannel succeeds with ed25519 seed', function () {
|
||||||
|
const wallet = Wallet.fromSeed('sEdSuqBPSQaood2DmNYVkwWTn1oQTj2')
|
||||||
|
const channelId =
|
||||||
|
'5DB01B7FFED6B67E6B0414DED11E051D2EE2B7619CE0EAA6286D67A3A4D5BDB3'
|
||||||
|
const amount = '1000000'
|
||||||
|
assert.equal(
|
||||||
|
authorizeChannel(wallet, channelId, amount),
|
||||||
|
'7E1C217A3E4B3C107B7A356E665088B4FBA6464C48C58267BEF64975E3375EA338AE22E6714E3F5E734AE33E6B97AAD59058E1E196C1F92346FC1498D0674404',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('verifySignature succeeds for valid signed transaction blob', function () {
|
||||||
|
const wallet = new Wallet(publicKey, privateKey)
|
||||||
|
|
||||||
|
const signedTx: string = sign(wallet, tx)
|
||||||
|
|
||||||
|
assert.isTrue(verifySignature(signedTx))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('verify succeeds for valid signed transaction object', function () {
|
||||||
|
const wallet = new Wallet(publicKey, privateKey)
|
||||||
|
|
||||||
|
const signedTx: string = sign(wallet, tx)
|
||||||
|
|
||||||
|
assert.isTrue(verifySignature(decode(signedTx) as unknown as Transaction))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('verify throws for invalid signing key', function () {
|
||||||
|
const wallet = new Wallet(publicKey, privateKey)
|
||||||
|
const signedTx: string = sign(wallet, tx)
|
||||||
|
|
||||||
|
const decodedTx: Transaction = decode(signedTx) as unknown as Transaction
|
||||||
|
|
||||||
|
// Use a different key for validation
|
||||||
|
decodedTx.SigningPubKey =
|
||||||
|
'0330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD020'
|
||||||
|
|
||||||
|
assert.isFalse(verifySignature(decodedTx))
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user