feat: add sidechain-specific RPCs/sugar (#1940)

* federator_info responses/requests

* edit docstring

* make strings narrower

* export federator_info

* implement createXchainPayment

* add tests

* fix dependency cycle

* fix linter errors

* rename xchain -> crosschain

* rename more

* edit HISTORY

* fix docstrings

* add another test
This commit is contained in:
Mayukha Vadari
2022-04-04 17:11:08 -04:00
committed by GitHub
parent 53d01d8a6a
commit d25905987a
7 changed files with 286 additions and 26 deletions

View File

@@ -3,6 +3,9 @@
Subscribe to [the **xrpl-announce** mailing list](https://groups.google.com/g/xrpl-announce) for release announcements. We recommend that xrpl.js (ripple-lib) users stay up-to-date with the latest stable release.
## Unreleased
### Added
* `federator_info` RPC support
* Helper method for creating a cross-chain payment to/from a sidechain
### Fixed
* Type of TrustSet transaction edited, specifically LimitAmount property type (fixed typescript issue)

View File

@@ -0,0 +1,78 @@
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

@@ -19,6 +19,7 @@ import {
DepositAuthorizedRequest,
DepositAuthorizedResponse,
} from './depositAuthorized'
import { FederatorInfoRequest, FederatorInfoResponse } from './federatorInfo'
import { FeeRequest, FeeResponse } from './fee'
import {
GatewayBalancesRequest,
@@ -116,6 +117,8 @@ type Request =
// NFT methods
| NFTBuyOffersRequest
| NFTSellOffersRequest
// sidechain methods
| FederatorInfoRequest
/**
* @category Responses
@@ -164,6 +167,8 @@ type Response =
// NFT methods
| NFTBuyOffersResponse
| NFTSellOffersResponse
// sidechain methods
| FederatorInfoResponse
export {
Request,
@@ -258,4 +263,7 @@ export {
NFTBuyOffersResponse,
NFTSellOffersRequest,
NFTSellOffersResponse,
// sidechain methods
FederatorInfoRequest,
FederatorInfoResponse,
}

View File

@@ -0,0 +1,40 @@
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

@@ -21,6 +21,7 @@ import { Response } from '../models/methods'
import { PaymentChannelClaim } from '../models/transactions/paymentChannelClaim'
import { Transaction } from '../models/transactions/transaction'
import createCrossChainPayment from './createCrossChainPayment'
import { deriveKeypair, deriveXAddress } from './derive'
import getBalanceChanges from './getBalanceChanges'
import {
@@ -46,6 +47,7 @@ import {
qualityToDecimal,
} from './quality'
import signPaymentChannelClaim from './signPaymentChannelClaim'
import { convertHexToString, convertStringToHex } from './stringConversion'
import {
rippleTimeToISOTime,
isoTimeToRippleTime,
@@ -134,32 +136,6 @@ function isValidAddress(address: string): boolean {
return isValidXAddress(address) || isValidClassicAddress(address)
}
/**
* Converts a string to its hex equivalent. Useful for Memos.
*
* @param string - The string to convert to Hex.
* @returns The Hex equivalent of the string.
* @category Utilities
*/
function convertStringToHex(string: string): string {
return Buffer.from(string, 'utf8').toString('hex').toUpperCase()
}
/**
* Converts hex to its string equivalent. Useful to read the Domain field and some Memos.
*
* @param hex - The hex to convert to a string.
* @param encoding - The encoding to use. Defaults to 'utf8' (UTF-8). 'ascii' is also allowed.
* @returns The converted string.
* @category Utilities
*/
function convertHexToString(
hex: string,
encoding: BufferEncoding = 'utf8',
): string {
return Buffer.from(hex, 'hex').toString(encoding)
}
/**
* Returns true if there are more pages of data.
*
@@ -238,4 +214,5 @@ export {
encodeForMultiSigning,
encodeForSigning,
encodeForSigningClaim,
createCrossChainPayment,
}

View File

@@ -0,0 +1,27 @@
/**
* Converts a string to its hex equivalent. Useful for Memos.
*
* @param string - The string to convert to Hex.
* @returns The Hex equivalent of the string.
* @category Utilities
*/
function convertStringToHex(string: string): string {
return Buffer.from(string, 'utf8').toString('hex').toUpperCase()
}
/**
* Converts hex to its string equivalent. Useful to read the Domain field and some Memos.
*
* @param hex - The hex to convert to a string.
* @param encoding - The encoding to use. Defaults to 'utf8' (UTF-8). 'ascii' is also allowed.
* @returns The converted string.
* @category Utilities
*/
function convertHexToString(
hex: string,
encoding: BufferEncoding = 'utf8',
): string {
return Buffer.from(hex, 'hex').toString(encoding)
}
export { convertHexToString, convertStringToHex }

View File

@@ -0,0 +1,127 @@
import { assert } from 'chai'
import {
createCrossChainPayment,
convertStringToHex,
Payment,
} from 'xrpl-local'
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)
})
})