Merge branch 'main' into sidechain-2.5

This commit is contained in:
Mayukha Vadari
2023-05-01 09:35:42 -04:00
16 changed files with 998 additions and 1286 deletions

View File

@@ -2,7 +2,11 @@
"editor.tabSize": 2,
"cSpell.words": [
"hostid",
"Multisigned",
"keypair",
"keypairs",
"multisign",
"multisigned",
"multisigning",
"preauthorization",
"secp256k1",
"Setf",

View File

@@ -74,6 +74,7 @@ It goes through:
If you're using xrpl.js with React or Deno, you'll need to do a couple extra steps to set it up:
- [Using xrpl.js with a CDN](./UNIQUE_SETUPS.md#using-xrpljs-from-a-cdn)
- [Using xrpl.js with `create-react-app`](./UNIQUE_SETUPS.md#using-xrpljs-with-create-react-app)
- [Using xrpl.js with `React Native`](./UNIQUE_SETUPS.md#using-xrpljs-with-react-native)
- [Using xrpl.js with `Vite React`](./UNIQUE_SETUPS.md#using-xrpljs-with-vite-react)

View File

@@ -2,6 +2,15 @@
For when you need to do more than just install `xrpl.js` for it to work (especially for React projects in the browser).
### Using xrpl.js from a CDN
You can avoid setting up your build system to handle `xrpl.js` by using a cdn version that is prebuilt for the browser.
- unpkg `<script src="https://unpkg.com/xrpl@2.3.0/build/xrpl-latest-min.js"></script>`
- jsdelivr `<script src="https://cdn.jsdelivr.net/npm/xrpl@2.3.0/build/xrpl-latest-min.js"></script>`
Ensure that the full path is provided so the browser can find the sourcemaps.
### Using xrpl.js with `create-react-app`
To use `xrpl.js` with React, you need to install shims for core NodeJS modules. Starting with version 5, Webpack stopped including shims by default, so you must modify your Webpack configuration to add the shims you need. Either you can eject your config and modify it, or you can use a library such as `react-app-rewired`. The example below uses `react-app-rewired`.

1809
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,7 +14,7 @@
"dependencies": {
"assert": "^2.0.0",
"big-integer": "^1.6.48",
"buffer": "5.6.0",
"buffer": "6.0.3",
"create-hash": "^1.2.0",
"decimal.js": "^10.2.0",
"ripple-address-codec": "^4.2.5"

View File

@@ -10,6 +10,10 @@ Subscribe to [the **xrpl-announce** mailing list](https://groups.google.com/g/xr
### Fixed
* Fixed `ServerState.transitions` typing, it is now a string instead of a number. (Only used in return from `server_state` request)
* Added `destination_amount` to `PathOption` which is returned as part of a `path_find` request
* Removed the `decode(encode(tx)) == tx` check from the wallet signing process
### Removed
* RPCs and utils related to the old sidechain design
## 2.7.0 (2023-03-08)

View File

@@ -45,7 +45,7 @@
"karma-webpack": "^5.0.0",
"node-polyfill-webpack-plugin": "^2.0.1",
"react": "^18.2.0",
"typedoc": "^0.23.24"
"typedoc": "^0.24.6"
},
"resolutions": {
"elliptic": "^6.5.4"

View File

@@ -1,8 +1,6 @@
/* eslint-disable max-lines -- There are lots of equivalent constructors which make sense to have here. */
import BigNumber from 'bignumber.js'
import { fromSeed } from 'bip32'
import { mnemonicToSeedSync, validateMnemonic } from 'bip39'
import isEqual from 'lodash/isEqual'
import omitBy from 'lodash/omitBy'
import {
classicAddressToXAddress,
@@ -25,11 +23,8 @@ import {
} from 'ripple-keypairs'
import ECDSA from '../ECDSA'
import { ValidationError, XrplError } from '../errors'
import { IssuedCurrencyAmount } from '../models/common'
import { ValidationError } from '../errors'
import { Transaction, validate } from '../models/transactions'
import { isIssuedCurrency } from '../models/transactions/common'
import { isHex } from '../models/utils'
import { ensureClassicAddress } from '../sugar/utils'
import { hashSignedTx } from '../utils/hashes/hashLedger'
@@ -368,7 +363,6 @@ class Wallet {
}
const serialized = encode(txToSignAndEncode)
this.checkTxSerialization(serialized, tx)
return {
tx_blob: serialized,
hash: hashSignedTx(serialized),
@@ -401,124 +395,6 @@ class Wallet {
public getXAddress(tag: number | false = false, isTestnet = false): string {
return classicAddressToXAddress(this.classicAddress, tag, isTestnet)
}
/**
* Decode a serialized transaction, remove the fields that are added during the signing process,
* and verify that it matches the transaction prior to signing. This gives the user a sanity check
* to ensure that what they try to encode matches the message that will be recieved by rippled.
*
* @param serialized - A signed and serialized transaction.
* @param tx - The transaction prior to signing.
* @throws A ValidationError if the transaction does not have a TxnSignature/Signers property, or if
* the serialized Transaction desn't match the original transaction.
* @throws XrplError if the transaction includes an issued currency which is equivalent to XRP ignoring case.
*/
// eslint-disable-next-line class-methods-use-this, max-lines-per-function -- Helper for organization purposes
private checkTxSerialization(serialized: string, tx: Transaction): void {
// Decode the serialized transaction:
const decoded = decode(serialized)
const txCopy = { ...tx }
/*
* And ensure it is equal to the original tx, except:
* - It must have a TxnSignature or Signers (multisign).
*/
if (!decoded.TxnSignature && !decoded.Signers) {
throw new ValidationError(
'Serialized transaction must have a TxnSignature or Signers property',
)
}
// - We know that the original tx did not have TxnSignature, so we should delete it:
delete decoded.TxnSignature
// - We know that the original tx did not have Signers, so if it exists, we should delete it:
delete decoded.Signers
/*
* - If SigningPubKey was not in the original tx, then we should delete it.
* But if it was in the original tx, then we should ensure that it has not been changed.
*/
if (!tx.SigningPubKey) {
delete decoded.SigningPubKey
}
/*
* - Memos have exclusively hex data which should ignore case.
* Since decode goes to upper case, we set all tx memos to be uppercase for the comparison.
*/
txCopy.Memos?.map((memo) => {
const memoCopy = { ...memo }
if (memo.Memo.MemoData) {
if (!isHex(memo.Memo.MemoData)) {
throw new ValidationError('MemoData field must be a hex value')
}
memoCopy.Memo.MemoData = memo.Memo.MemoData.toUpperCase()
}
if (memo.Memo.MemoType) {
if (!isHex(memo.Memo.MemoType)) {
throw new ValidationError('MemoType field must be a hex value')
}
memoCopy.Memo.MemoType = memo.Memo.MemoType.toUpperCase()
}
if (memo.Memo.MemoFormat) {
if (!isHex(memo.Memo.MemoFormat)) {
throw new ValidationError('MemoFormat field must be a hex value')
}
memoCopy.Memo.MemoFormat = memo.Memo.MemoFormat.toUpperCase()
}
return memo
})
if (txCopy.TransactionType === 'NFTokenMint' && txCopy.URI) {
txCopy.URI = txCopy.URI.toUpperCase()
}
/* eslint-disable @typescript-eslint/consistent-type-assertions -- We check at runtime that this is safe */
Object.keys(txCopy).forEach((key) => {
const standard_currency_code_len = 3
if (txCopy[key] && isIssuedCurrency(txCopy[key])) {
const decodedAmount = decoded[key] as unknown as IssuedCurrencyAmount
const decodedCurrency = decodedAmount.currency
const txCurrency = (txCopy[key] as IssuedCurrencyAmount).currency
if (
txCurrency.length === standard_currency_code_len &&
txCurrency.toUpperCase() === 'XRP'
) {
throw new XrplError(
`Trying to sign an issued currency with a similar standard code to XRP (received '${txCurrency}'). XRP is not an issued currency.`,
)
}
// Standardize the format of currency codes to the 40 byte hex string for comparison
const amount = txCopy[key] as IssuedCurrencyAmount
if (amount.currency.length !== decodedCurrency.length) {
/* eslint-disable-next-line max-depth -- Easier to read with two if-statements */
if (decodedCurrency.length === standard_currency_code_len) {
decodedAmount.currency = isoToHex(decodedCurrency)
} else {
/* eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- We need to update txCopy directly */
txCopy[key].currency = isoToHex(txCopy[key].currency)
}
}
}
})
/* eslint-enable @typescript-eslint/consistent-type-assertions -- Done with dynamic checking */
if (!isEqual(decoded, txCopy)) {
const data = {
decoded,
tx,
}
const error = new ValidationError(
'Serialized transaction does not match original txJSON. See error.data',
data,
)
throw error
}
}
}
/**
@@ -567,20 +443,4 @@ function removeTrailingZeros(tx: Transaction): void {
}
}
/**
* Convert an ISO code to a hex string representation
*
* @param iso - A 3 letter standard currency code
*/
/* eslint-disable @typescript-eslint/no-magic-numbers -- Magic numbers are from rippleds of currency code encoding */
function isoToHex(iso: string): string {
const bytes = Buffer.alloc(20)
if (iso !== 'XRP') {
const isoBytes = iso.split('').map((chr) => chr.charCodeAt(0))
bytes.set(isoBytes, 12)
}
return bytes.toString('hex').toUpperCase()
}
/* eslint-enable @typescript-eslint/no-magic-numbers -- Only needed in this function */
export default Wallet

View File

@@ -1,78 +0,0 @@
import { BaseRequest, BaseResponse } from './baseMethod'
/**
* The `federator_info` command asks the federator for information
* about the door account and other bridge-related information. This
* method only exists on sidechain federators. Expects a response in
* the form of a {@link FederatorInfoResponse}.
*
* @category Requests
*/
export interface FederatorInfoRequest extends BaseRequest {
command: 'federator_info'
}
/**
* Response expected from a {@link FederatorInfoRequest}.
*
* @category Responses
*/
export interface FederatorInfoResponse extends BaseResponse {
result: {
info: {
mainchain: {
door_status: {
initialized: boolean
status: 'open' | 'opening' | 'closed' | 'closing'
}
last_transaction_sent_seq: number
listener_info: {
state: 'syncing' | 'normal'
}
pending_transactions: Array<{
amount: string
destination_account: string
signatures: Array<{
public_key: string
seq: number
}>
}>
sequence: number
tickets: {
initialized: boolean
tickets: Array<{
status: 'taken' | 'available'
ticket_seq: number
}>
}
}
public_key: string
sidechain: {
door_status: {
initialized: boolean
status: 'open' | 'opening' | 'closed' | 'closing'
}
last_transaction_sent_seq: number
listener_info: {
state: 'syncing' | 'normal'
}
pending_transactions: Array<{
amount: string
destination_account: string
signatures: Array<{
public_key: string
seq: number
}>
}>
sequence: number
tickets: {
initialized: boolean
tickets: Array<{
status: 'taken' | 'available'
ticket_seq: number
}>
}
}
}
}
}

View File

@@ -23,7 +23,6 @@ import {
DepositAuthorizedRequest,
DepositAuthorizedResponse,
} from './depositAuthorized'
import { FederatorInfoRequest, FederatorInfoResponse } from './federatorInfo'
import { FeeRequest, FeeResponse } from './fee'
import {
GatewayBalancesRequest,
@@ -121,8 +120,6 @@ type Request =
// NFT methods
| NFTBuyOffersRequest
| NFTSellOffersRequest
// sidechain methods
| FederatorInfoRequest
/**
* @category Responses
@@ -171,8 +168,6 @@ type Response =
// NFT methods
| NFTBuyOffersResponse
| NFTSellOffersResponse
// sidechain methods
| FederatorInfoResponse
export {
Request,
@@ -268,7 +263,4 @@ export {
NFTBuyOffersResponse,
NFTSellOffersRequest,
NFTSellOffersResponse,
// sidechain methods
FederatorInfoRequest,
FederatorInfoResponse,
}

View File

@@ -2,6 +2,8 @@
/* eslint-disable max-lines-per-function -- need to work with a lot of Tx verifications */
import { ValidationError } from '../../errors'
import { IssuedCurrencyAmount, Memo } from '../common'
import { isHex } from '../utils'
import { setTransactionFlagsToNumber } from '../utils/flags'
import { AccountDelete, validateAccountDelete } from './accountDelete'
@@ -9,6 +11,7 @@ import { AccountSet, validateAccountSet } from './accountSet'
import { CheckCancel, validateCheckCancel } from './checkCancel'
import { CheckCash, validateCheckCash } from './checkCash'
import { CheckCreate, validateCheckCreate } from './checkCreate'
import { isIssuedCurrency } from './common'
import { DepositPreauth, validateDepositPreauth } from './depositPreauth'
import { EscrowCancel, validateEscrowCancel } from './escrowCancel'
import { EscrowCreate, validateEscrowCreate } from './escrowCreate'
@@ -135,6 +138,56 @@ export function validate(transaction: Record<string, unknown>): void {
if (typeof tx.TransactionType !== 'string') {
throw new ValidationError("Object's `TransactionType` is not a string")
}
/*
* - Memos have exclusively hex data.
*/
if (tx.Memos != null && typeof tx.Memos !== 'object') {
throw new ValidationError('Memo must be array')
}
if (tx.Memos != null) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- needed here
;(tx.Memos as Array<Memo | null>).forEach((memo) => {
if (memo?.Memo == null) {
throw new ValidationError('Memo data must be in a `Memo` field')
}
if (memo.Memo.MemoData) {
if (!isHex(memo.Memo.MemoData)) {
throw new ValidationError('MemoData field must be a hex value')
}
}
if (memo.Memo.MemoType) {
if (!isHex(memo.Memo.MemoType)) {
throw new ValidationError('MemoType field must be a hex value')
}
}
if (memo.Memo.MemoFormat) {
if (!isHex(memo.Memo.MemoFormat)) {
throw new ValidationError('MemoFormat field must be a hex value')
}
}
})
}
Object.keys(tx).forEach((key) => {
const standard_currency_code_len = 3
if (tx[key] && isIssuedCurrency(tx[key])) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- needed
const txCurrency = (tx[key] as IssuedCurrencyAmount).currency
if (
txCurrency.length === standard_currency_code_len &&
txCurrency.toUpperCase() === 'XRP'
) {
throw new ValidationError(
`Cannot have an issued currency with a similar standard code to XRP (received '${txCurrency}'). XRP is not an issued currency.`,
)
}
}
})
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- okay here
setTransactionFlagsToNumber(tx as unknown as Transaction)
switch (tx.TransactionType) {

View File

@@ -1,40 +0,0 @@
import { XrplError } from '../errors'
import { Payment } from '../models'
import { Memo } from '../models/common'
import { convertStringToHex } from './stringConversion'
/**
* Creates a cross-chain payment transaction.
*
* @param payment - The initial payment transaction. If the transaction is
* signed, then it will need to be re-signed. There must be no more than 2
* memos, since one memo is used for the sidechain destination account. The
* destination must be the sidechain's door account.
* @param destAccount - the destination account on the sidechain.
* @returns A cross-chain payment transaction, where the mainchain door account
* is the `Destination` and the destination account on the sidechain is encoded
* in the memos.
* @throws XrplError - if there are more than 2 memos.
* @category Utilities
*/
export default function createCrossChainPayment(
payment: Payment,
destAccount: string,
): Payment {
const destAccountHex = convertStringToHex(destAccount)
const destAccountMemo: Memo = { Memo: { MemoData: destAccountHex } }
const memos = payment.Memos ?? []
if (memos.length > 2) {
throw new XrplError(
'Cannot have more than 2 memos in a cross-chain transaction.',
)
}
const newMemos = [destAccountMemo, ...memos]
const newPayment = { ...payment, Memos: newMemos }
delete newPayment.TxnSignature
return newPayment
}

View File

@@ -22,7 +22,6 @@ import { Response } from '../models/methods'
import { PaymentChannelClaim } from '../models/transactions/paymentChannelClaim'
import { Transaction } from '../models/transactions/transaction'
import createCrossChainPayment from './createCrossChainPayment'
import { deriveKeypair, deriveAddress, deriveXAddress } from './derive'
import getBalanceChanges from './getBalanceChanges'
import getNFTokenID from './getNFTokenID'
@@ -220,6 +219,5 @@ export {
encodeForSigning,
encodeForSigningClaim,
getNFTokenID,
createCrossChainPayment,
parseNFTokenID,
}

View File

@@ -16,7 +16,7 @@ describe('TrustSet', function () {
TransactionType: 'TrustSet',
Account: 'rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo',
LimitAmount: {
currency: 'XRP',
currency: 'USD',
issuer: 'rcXY84C4g14iFp6taFXjjQGVeHqSCh9RX',
value: '4329.23',
},

View File

@@ -1,124 +0,0 @@
import { assert } from 'chai'
import { createCrossChainPayment, convertStringToHex, Payment } from '../../src'
describe('createCrossChainPayment', function () {
it('successful xchain payment creation', function () {
const payment: Payment = {
TransactionType: 'Payment',
Account: 'rRandom',
Destination: 'rRandom2',
Amount: '3489303',
}
const sidechainAccount = 'rSidechain'
const expectedPayment = {
...payment,
Memos: [
{
Memo: {
MemoData: convertStringToHex(sidechainAccount),
},
},
],
}
const resultPayment = createCrossChainPayment(payment, sidechainAccount)
assert.deepEqual(resultPayment, expectedPayment)
// ensure that the original object wasn't modified
assert.notDeepEqual(resultPayment, payment)
})
it('successful xchain payment creation with memo', function () {
const memo = {
Memo: {
MemoData: 'deadbeef',
},
}
const payment: Payment = {
TransactionType: 'Payment',
Account: 'rRandom',
Destination: 'rRandom2',
Amount: '3489303',
Memos: [memo],
}
const sidechainAccount = 'rSidechain'
const expectedPayment = {
...payment,
Memos: [
{
Memo: {
MemoData: convertStringToHex(sidechainAccount),
},
},
memo,
],
}
const resultPayment = createCrossChainPayment(payment, sidechainAccount)
assert.deepEqual(resultPayment, expectedPayment)
// ensure that the original object wasn't modified
assert.notDeepEqual(resultPayment, payment)
})
it('removes TxnSignature', function () {
const payment: Payment = {
TransactionType: 'Payment',
Account: 'rRandom',
Destination: 'rRandom2',
Amount: '3489303',
TxnSignature: 'asodfiuaosdfuaosd',
}
const sidechainAccount = 'rSidechain'
const expectedPayment = {
...payment,
Memos: [
{
Memo: {
MemoData: convertStringToHex(sidechainAccount),
},
},
],
}
delete expectedPayment.TxnSignature
const resultPayment = createCrossChainPayment(payment, sidechainAccount)
assert.deepEqual(resultPayment, expectedPayment)
// ensure that the original object wasn't modified
assert.notDeepEqual(resultPayment, payment)
})
it('fails with 3 memos', function () {
const payment: Payment = {
TransactionType: 'Payment',
Account: 'rRandom',
Destination: 'rRandom2',
Amount: '3489303',
Memos: [
{
Memo: {
MemoData: '2934723843ace',
},
},
{
Memo: {
MemoData: '2934723843ace',
},
},
{
Memo: {
MemoData: '2934723843ace',
},
},
],
}
assert.throws(() => {
createCrossChainPayment(payment, 'rSidechain')
}, /Cannot have more than 2 memos/u)
})
})

View File

@@ -763,7 +763,7 @@ describe('Wallet', function () {
}
assert.throws(() => {
wallet.sign(payment)
}, /^Trying to sign an issued currency with a similar standard code to XRP \(received 'xrp'\)\. XRP is not an issued currency\./u)
}, /^Cannot have an issued currency with a similar standard code to XRP \(received 'xrp'\)\. XRP is not an issued currency\./u)
})
it('sign does NOT throw when a payment contains an issued currency like xrp in hex string format', async function () {