mirror of
https://github.com/Xahau/xahau.js.git
synced 2025-11-05 05:15:48 +00:00
Compare commits
41 Commits
ripple-key
...
network-id
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3f5b3a844 | ||
|
|
17fcddf7bb | ||
|
|
5f5065480a | ||
|
|
fd550a5bd2 | ||
|
|
1264d45844 | ||
|
|
8c3ef3c2bd | ||
|
|
2e3bc5d9bb | ||
|
|
acf76f3c24 | ||
|
|
2fa361cda6 | ||
|
|
3a865eba5f | ||
|
|
d7d6ae873d | ||
|
|
a4a6306ade | ||
|
|
a940b7c408 | ||
|
|
e85d077d24 | ||
|
|
6e4531b3c3 | ||
|
|
7d54c1e059 | ||
|
|
faea1abafb | ||
|
|
cabe79d4c0 | ||
|
|
1d9f9ae7c5 | ||
|
|
56cd415a12 | ||
|
|
f6d26bf0b7 | ||
|
|
7f078b6a3b | ||
|
|
261669b346 | ||
|
|
258f9a391a | ||
|
|
ce0e6e103a | ||
|
|
e656de772a | ||
|
|
6c7d2538e9 | ||
|
|
bf63fd0173 | ||
|
|
aed48e77bc | ||
|
|
b26a46b020 | ||
|
|
cbd4f9f350 | ||
|
|
37c03460af | ||
|
|
55fe97d53d | ||
|
|
07ff7630b1 | ||
|
|
a5deee1274 | ||
|
|
294c1cb083 | ||
|
|
69c705874f | ||
|
|
e36912c60a | ||
|
|
3d06185867 | ||
|
|
b241779f10 | ||
|
|
c809bd87e4 |
@@ -321,6 +321,16 @@
|
||||
"type": "UInt16"
|
||||
}
|
||||
],
|
||||
[
|
||||
"NetworkID",
|
||||
{
|
||||
"nth": 1,
|
||||
"isVLEncoded": false,
|
||||
"isSerialized": true,
|
||||
"isSigningField": true,
|
||||
"type": "UInt32"
|
||||
}
|
||||
],
|
||||
[
|
||||
"Flags",
|
||||
{
|
||||
@@ -2176,6 +2186,9 @@
|
||||
"telCAN_NOT_QUEUE_BLOCKED": -389,
|
||||
"telCAN_NOT_QUEUE_FEE": -388,
|
||||
"telCAN_NOT_QUEUE_FULL": -387,
|
||||
"telWRONG_NETWORK": -386,
|
||||
"telREQUIRES_NETWORK_ID": -385,
|
||||
"telNETWORK_ID_MAKES_TX_NON_CANONICAL": -384,
|
||||
|
||||
"temMALFORMED": -299,
|
||||
"temBAD_AMOUNT": -298,
|
||||
|
||||
@@ -205,6 +205,18 @@ class Client extends EventEmitter {
|
||||
*/
|
||||
public readonly maxFeeXRP: string
|
||||
|
||||
/**
|
||||
* Network ID of the server this client is connected to
|
||||
*
|
||||
*/
|
||||
public networkID: number | undefined
|
||||
|
||||
/**
|
||||
* Rippled Version used by the server this client is connected to
|
||||
*
|
||||
*/
|
||||
public buildVersion: string | undefined
|
||||
|
||||
/**
|
||||
* Creates a new Client with a websocket connection to a rippled server.
|
||||
*
|
||||
@@ -230,8 +242,8 @@ class Client extends EventEmitter {
|
||||
this.emit('error', errorCode, errorMessage, data)
|
||||
})
|
||||
|
||||
this.connection.on('connected', () => {
|
||||
this.emit('connected')
|
||||
this.connection.on('reconnect', () => {
|
||||
this.connection.on('connected', () => this.emit('connected'))
|
||||
})
|
||||
|
||||
this.connection.on('disconnected', (code: number) => {
|
||||
@@ -568,6 +580,22 @@ class Client extends EventEmitter {
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Get networkID and buildVersion from server_info
|
||||
*/
|
||||
public async getServerInfo(): Promise<void> {
|
||||
try {
|
||||
const response = await this.request({
|
||||
command: 'server_info',
|
||||
})
|
||||
this.networkID = response.result.info.network_id ?? undefined
|
||||
this.buildVersion = response.result.info.build_version
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console -- Print the error to console but allows client to be connected.
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the Client instance to connect to its rippled server.
|
||||
*
|
||||
@@ -588,7 +616,10 @@ class Client extends EventEmitter {
|
||||
* @category Network
|
||||
*/
|
||||
public async connect(): Promise<void> {
|
||||
return this.connection.connect()
|
||||
return this.connection.connect().then(async () => {
|
||||
await this.getServerInfo()
|
||||
this.emit('connected')
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -136,6 +136,10 @@ export interface ServerInfoResponse extends BaseResponse {
|
||||
* overall network's load factor.
|
||||
*/
|
||||
load_factor?: number
|
||||
/**
|
||||
* The network id of the server.
|
||||
*/
|
||||
network_id?: number
|
||||
/**
|
||||
* Current multiplier to the transaction cost based on
|
||||
* load to this server.
|
||||
|
||||
@@ -157,6 +157,10 @@ export interface BaseTransaction {
|
||||
* account it says it is from.
|
||||
*/
|
||||
TxnSignature?: string
|
||||
/**
|
||||
* The network id of the transaction.
|
||||
*/
|
||||
NetworkID?: number
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -250,6 +254,9 @@ export function validateBaseTransaction(common: Record<string, unknown>): void {
|
||||
) {
|
||||
throw new ValidationError('BaseTransaction: invalid TxnSignature')
|
||||
}
|
||||
if (common.NetworkID !== undefined && typeof common.NetworkID !== 'number') {
|
||||
throw new ValidationError('BaseTransaction: invalid NetworkID')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,6 +12,13 @@ import getFeeXrp from './getFeeXrp'
|
||||
|
||||
// Expire unconfirmed transactions after 20 ledger versions, approximately 1 minute, by default
|
||||
const LEDGER_OFFSET = 20
|
||||
// Sidechains are expected to have network IDs above this.
|
||||
// Networks with ID above this restricted number are expected specify an accurate NetworkID field
|
||||
// in every transaction to that chain to prevent replay attacks.
|
||||
// Mainnet and testnet are exceptions. More context: https://github.com/XRPLF/rippled/pull/4370
|
||||
const RESTRICTED_NETWORKS = 1024
|
||||
const REQUIRED_NETWORKID_VERSION = '1.11.0'
|
||||
const HOOKS_TESTNET_ID = 21338
|
||||
interface ClassicAccountAndTag {
|
||||
classicAccount: string
|
||||
tag: number | false | undefined
|
||||
@@ -70,8 +77,10 @@ async function autofill<T extends Transaction>(
|
||||
setValidAddresses(tx)
|
||||
|
||||
setTransactionFlagsToNumber(tx)
|
||||
|
||||
const promises: Array<Promise<void>> = []
|
||||
if (tx.NetworkID == null) {
|
||||
tx.NetworkID = txNeedsNetworkID(this) ? this.networkID : undefined
|
||||
}
|
||||
if (tx.Sequence == null) {
|
||||
promises.push(setNextValidSequenceNumber(this, tx))
|
||||
}
|
||||
@@ -88,6 +97,101 @@ async function autofill<T extends Transaction>(
|
||||
return Promise.all(promises).then(() => tx)
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the source rippled version is not later than the target rippled version.
|
||||
* Example usage: isNotLaterRippledVersion('1.10.0', '1.11.0') returns true.
|
||||
* isNotLaterRippledVersion('1.10.0', '1.10.0-b1') returns false.
|
||||
*
|
||||
* @param source -- The source rippled version.
|
||||
* @param target -- The target rippled version.
|
||||
* @returns True if source is earlier than target, false otherwise.
|
||||
*/
|
||||
// eslint-disable-next-line max-lines-per-function, max-statements -- Disable for this helper functions.
|
||||
function isNotLaterRippledVersion(source: string, target: string): boolean {
|
||||
if (source === target) {
|
||||
return true
|
||||
}
|
||||
const sourceDecomp = source.split('.')
|
||||
const targetDecomp = target.split('.')
|
||||
const sourceMajor = parseInt(sourceDecomp[0], 10)
|
||||
const sourceMinor = parseInt(sourceDecomp[1], 10)
|
||||
const targetMajor = parseInt(targetDecomp[0], 10)
|
||||
const targetMinor = parseInt(targetDecomp[1], 10)
|
||||
// Compare major version
|
||||
if (sourceMajor !== targetMajor) {
|
||||
return sourceMajor < targetMajor
|
||||
}
|
||||
// Compare minor version
|
||||
if (sourceMinor !== targetMinor) {
|
||||
return sourceMinor < targetMinor
|
||||
}
|
||||
const sourcePatch = sourceDecomp[2].split('-')
|
||||
const targetPatch = targetDecomp[2].split('-')
|
||||
|
||||
const sourcePatchVersion = parseInt(sourcePatch[0], 10)
|
||||
const targetPatchVersion = parseInt(targetPatch[0], 10)
|
||||
|
||||
// Compare patch version
|
||||
if (sourcePatchVersion !== targetPatchVersion) {
|
||||
return sourcePatchVersion < targetPatchVersion
|
||||
}
|
||||
|
||||
// Compare release version
|
||||
if (sourcePatch.length !== targetPatch.length) {
|
||||
return sourcePatch.length > targetPatch.length
|
||||
}
|
||||
|
||||
if (sourcePatch.length === 2) {
|
||||
// Compare different release types
|
||||
if (!sourcePatch[1][0].startsWith(targetPatch[1][0])) {
|
||||
return sourcePatch[1] < targetPatch[1]
|
||||
}
|
||||
// Compare beta version
|
||||
if (sourcePatch[1].startsWith('b')) {
|
||||
return (
|
||||
parseInt(sourcePatch[1].slice(1), 10) <
|
||||
parseInt(targetPatch[1].slice(1), 10)
|
||||
)
|
||||
}
|
||||
// Compare rc version
|
||||
return (
|
||||
parseInt(sourcePatch[1].slice(2), 10) <
|
||||
parseInt(targetPatch[1].slice(2), 10)
|
||||
)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the transaction required a networkID to be valid.
|
||||
* Transaction needs networkID if later than restricted ID and either the network is hooks testnet
|
||||
* or build version is >= 1.11.0
|
||||
*
|
||||
* @param client -- The connected client.
|
||||
* @returns True if required networkID, false otherwise.
|
||||
*/
|
||||
function txNeedsNetworkID(client: Client): boolean {
|
||||
if (
|
||||
client.networkID !== undefined &&
|
||||
client.networkID > RESTRICTED_NETWORKS
|
||||
) {
|
||||
// TODO: remove the buildVersion logic when 1.11.0 is out and widely used.
|
||||
// Issue: https://github.com/XRPLF/xrpl.js/issues/2339
|
||||
if (
|
||||
(client.buildVersion &&
|
||||
isNotLaterRippledVersion(
|
||||
REQUIRED_NETWORKID_VERSION,
|
||||
client.buildVersion,
|
||||
)) ||
|
||||
client.networkID === HOOKS_TESTNET_ID
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function setValidAddresses(tx: Transaction): void {
|
||||
validateAccountAddress(tx, 'Account', 'SourceTag')
|
||||
// eslint-disable-next-line @typescript-eslint/dot-notation -- Destination can exist on Transaction
|
||||
|
||||
@@ -15,13 +15,32 @@ import {
|
||||
} from '../setupClient'
|
||||
import { assertRejects } from '../testUtils'
|
||||
|
||||
const NetworkID = 1025
|
||||
const Fee = '10'
|
||||
const Sequence = 1432
|
||||
const LastLedgerSequence = 2908734
|
||||
const HOOKS_TESTNET_ID = 21338
|
||||
|
||||
describe('client.autofill', function () {
|
||||
let testContext: XrplTestContext
|
||||
|
||||
async function setupMockRippledVersionAndID(
|
||||
buildVersion: string,
|
||||
networkID: number,
|
||||
): Promise<void> {
|
||||
await testContext.client.disconnect()
|
||||
rippled.server_info.withNetworkId.result.info.build_version = buildVersion
|
||||
rippled.server_info.withNetworkId.result.info.network_id = networkID
|
||||
testContext.client.connection.on('connected', () => {
|
||||
testContext.mockRippled?.addResponse(
|
||||
'server_info',
|
||||
rippled.server_info.withNetworkId,
|
||||
)
|
||||
})
|
||||
|
||||
await testContext.client.connect()
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
testContext = await setupClient()
|
||||
})
|
||||
@@ -32,17 +51,116 @@ describe('client.autofill', function () {
|
||||
TransactionType: 'DepositPreauth',
|
||||
Account: 'rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf',
|
||||
Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo',
|
||||
NetworkID,
|
||||
Fee,
|
||||
Sequence,
|
||||
LastLedgerSequence,
|
||||
}
|
||||
const txResult = await testContext.client.autofill(tx)
|
||||
|
||||
assert.strictEqual(txResult.NetworkID, NetworkID)
|
||||
assert.strictEqual(txResult.Fee, Fee)
|
||||
assert.strictEqual(txResult.Sequence, Sequence)
|
||||
assert.strictEqual(txResult.LastLedgerSequence, LastLedgerSequence)
|
||||
})
|
||||
|
||||
it('ignores network ID if missing', async function () {
|
||||
const tx: Payment = {
|
||||
TransactionType: 'Payment',
|
||||
Account: 'XVLhHMPHU98es4dbozjVtdWzVrDjtV18pX8yuPT7y4xaEHi',
|
||||
Amount: '1234',
|
||||
Destination: 'X7AcgcsBL6XDcUb289X4mJ8djcdyKaB5hJDWMArnXr61cqZ',
|
||||
Fee,
|
||||
Sequence,
|
||||
LastLedgerSequence,
|
||||
}
|
||||
testContext.mockRippled!.addResponse('ledger', rippled.ledger.normal)
|
||||
|
||||
const txResult = await testContext.client.autofill(tx)
|
||||
|
||||
assert.strictEqual(txResult.NetworkID, undefined)
|
||||
})
|
||||
|
||||
// NetworkID is required in transaction for network > 1024 and from version 1.11.0 or later.
|
||||
// More context: https://github.com/XRPLF/rippled/pull/4370
|
||||
it('overrides network ID if > 1024 and version is later than 1.11.0', async function () {
|
||||
await setupMockRippledVersionAndID('1.11.1', 1025)
|
||||
const tx: Payment = {
|
||||
TransactionType: 'Payment',
|
||||
Account: 'XVLhHMPHU98es4dbozjVtdWzVrDjtV18pX8yuPT7y4xaEHi',
|
||||
Amount: '1234',
|
||||
Destination: 'X7AcgcsBL6XDcUb289X4mJ8djcdyKaB5hJDWMArnXr61cqZ',
|
||||
Fee,
|
||||
Sequence,
|
||||
LastLedgerSequence,
|
||||
}
|
||||
testContext.mockRippled!.addResponse('ledger', rippled.ledger.normal)
|
||||
|
||||
const txResult = await testContext.client.autofill(tx)
|
||||
|
||||
assert.strictEqual(txResult.NetworkID, 1025)
|
||||
})
|
||||
|
||||
// NetworkID is only required in transaction for version 1.11.0 or later.
|
||||
// More context: https://github.com/XRPLF/rippled/pull/4370
|
||||
it('ignores network ID if > 1024 but version is earlier than 1.11.0', async function () {
|
||||
await setupMockRippledVersionAndID('1.10.0', 1025)
|
||||
const tx: Payment = {
|
||||
TransactionType: 'Payment',
|
||||
Account: 'XVLhHMPHU98es4dbozjVtdWzVrDjtV18pX8yuPT7y4xaEHi',
|
||||
Amount: '1234',
|
||||
Destination: 'X7AcgcsBL6XDcUb289X4mJ8djcdyKaB5hJDWMArnXr61cqZ',
|
||||
Fee,
|
||||
Sequence,
|
||||
LastLedgerSequence,
|
||||
}
|
||||
testContext.mockRippled!.addResponse('ledger', rippled.ledger.normal)
|
||||
|
||||
const txResult = await testContext.client.autofill(tx)
|
||||
|
||||
assert.strictEqual(txResult.NetworkID, undefined)
|
||||
})
|
||||
|
||||
// NetworkID <= 1024 does not require a newtorkID in transaction.
|
||||
// More context: https://github.com/XRPLF/rippled/pull/4370
|
||||
it('ignores network ID if <= 1024', async function () {
|
||||
await setupMockRippledVersionAndID('1.11.1', 1023)
|
||||
const tx: Payment = {
|
||||
TransactionType: 'Payment',
|
||||
Account: 'XVLhHMPHU98es4dbozjVtdWzVrDjtV18pX8yuPT7y4xaEHi',
|
||||
Amount: '1234',
|
||||
Destination: 'X7AcgcsBL6XDcUb289X4mJ8djcdyKaB5hJDWMArnXr61cqZ',
|
||||
Fee,
|
||||
Sequence,
|
||||
LastLedgerSequence,
|
||||
}
|
||||
testContext.mockRippled!.addResponse('ledger', rippled.ledger.normal)
|
||||
|
||||
const txResult = await testContext.client.autofill(tx)
|
||||
|
||||
assert.strictEqual(txResult.NetworkID, undefined)
|
||||
})
|
||||
|
||||
// Hooks Testnet requires networkID in transaction regardless of version.
|
||||
// More context: https://github.com/XRPLF/rippled/pull/4370
|
||||
it('overrides network ID for hooks testnet', async function () {
|
||||
await setupMockRippledVersionAndID('1.10.1', HOOKS_TESTNET_ID)
|
||||
const tx: Payment = {
|
||||
TransactionType: 'Payment',
|
||||
Account: 'XVLhHMPHU98es4dbozjVtdWzVrDjtV18pX8yuPT7y4xaEHi',
|
||||
Amount: '1234',
|
||||
Destination: 'X7AcgcsBL6XDcUb289X4mJ8djcdyKaB5hJDWMArnXr61cqZ',
|
||||
Fee,
|
||||
Sequence,
|
||||
LastLedgerSequence,
|
||||
}
|
||||
testContext.mockRippled!.addResponse('ledger', rippled.ledger.normal)
|
||||
|
||||
const txResult = await testContext.client.autofill(tx)
|
||||
|
||||
assert.strictEqual(txResult.NetworkID, HOOKS_TESTNET_ID)
|
||||
})
|
||||
|
||||
it('converts Account & Destination X-address to their classic address', async function () {
|
||||
const tx: Payment = {
|
||||
TransactionType: 'Payment',
|
||||
|
||||
@@ -10,7 +10,7 @@ import type {
|
||||
|
||||
import { destroyServer, getFreePort } from './testUtils'
|
||||
|
||||
function createResponse(
|
||||
export function createResponse(
|
||||
request: { id: number | string },
|
||||
response: Record<string, unknown>,
|
||||
): string {
|
||||
|
||||
2
packages/xrpl/test/fixtures/rippled/index.ts
vendored
2
packages/xrpl/test/fixtures/rippled/index.ts
vendored
@@ -12,6 +12,7 @@ import iouPartialPayment from './partialPaymentIOU.json'
|
||||
import xrpPartialPayment from './partialPaymentXRP.json'
|
||||
import normalServerInfo from './serverInfo.json'
|
||||
import highLoadFactor from './serverInfoHighLoadFactor.json'
|
||||
import withNetworkIDServerInfo from './serverInfoNetworkID.json'
|
||||
import consensusStream from './streams/consensusPhase.json'
|
||||
import ledgerStream from './streams/ledger.json'
|
||||
import manifestStream from './streams/manifest.json'
|
||||
@@ -84,6 +85,7 @@ const ledger_data = {
|
||||
const server_info = {
|
||||
normal: normalServerInfo,
|
||||
highLoadFactor,
|
||||
withNetworkId: withNetworkIDServerInfo,
|
||||
}
|
||||
|
||||
const tx = {
|
||||
|
||||
31
packages/xrpl/test/fixtures/rippled/serverInfoNetworkID.json
vendored
Normal file
31
packages/xrpl/test/fixtures/rippled/serverInfoNetworkID.json
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"id": 0,
|
||||
"status": "success",
|
||||
"type": "response",
|
||||
"result": {
|
||||
"info": {
|
||||
"build_version": "1.11.0-rc2",
|
||||
"complete_ledgers": "37621036-38327626",
|
||||
"hostid": "JANE",
|
||||
"io_latency_ms": 1,
|
||||
"last_close": {
|
||||
"converge_time_s": 2,
|
||||
"proposers": 6
|
||||
},
|
||||
"load_factor": 1,
|
||||
"network_id": 1,
|
||||
"peers": 113,
|
||||
"pubkey_node": "n9L6MAkAvZKakewLSJPkCKLxuSQ9jrYXJBd2L4fouhpXauyFh6ZM",
|
||||
"server_state": "full",
|
||||
"validated_ledger": {
|
||||
"age": 0,
|
||||
"base_fee_xrp": 0.00001,
|
||||
"hash": "A219F66BB8C9992E80A3C93A5EA408CD54B8F47F2AC1246C271C495F833752BA",
|
||||
"reserve_base_xrp": 10,
|
||||
"reserve_inc_xrp": 2,
|
||||
"seq": 38327626
|
||||
},
|
||||
"validation_quorum": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ describe('mock rippled tests', function () {
|
||||
}
|
||||
|
||||
await assertRejects(
|
||||
testContext.client.request({ command: 'server_info' }),
|
||||
testContext.client.request({ command: 'account_info' }),
|
||||
RippledError,
|
||||
)
|
||||
})
|
||||
|
||||
@@ -232,4 +232,17 @@ describe('BaseTransaction', function () {
|
||||
'BaseTransaction: invalid Memos',
|
||||
)
|
||||
})
|
||||
|
||||
it(`Handles invalid NetworkID`, function () {
|
||||
const invalidNetworkID = {
|
||||
Account: 'r97KeayHuEsDwyU1yPBVtMLLoQr79QcRFe',
|
||||
TransactionType: 'Payment',
|
||||
NetworkID: '1024',
|
||||
}
|
||||
assert.throws(
|
||||
() => validateBaseTransaction(invalidNetworkID),
|
||||
ValidationError,
|
||||
'BaseTransaction: invalid NetworkID',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,6 +5,7 @@ import BroadcastClient from '../src/client/BroadcastClient'
|
||||
import createMockRippled, {
|
||||
type MockedWebSocketServer,
|
||||
} from './createMockRippled'
|
||||
import rippled from './fixtures/rippled'
|
||||
import { destroyServer, getFreePort } from './testUtils'
|
||||
|
||||
export interface XrplTestContext {
|
||||
@@ -29,6 +30,10 @@ async function setupMockRippledConnection(
|
||||
context.client.on('error', () => {
|
||||
// We must have an error listener attached for reconnect errors
|
||||
})
|
||||
context.mockRippled?.addResponse(
|
||||
'server_info',
|
||||
rippled.server_info.withNetworkId,
|
||||
)
|
||||
|
||||
return context.client.connect().then(() => context)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user