mirror of
https://github.com/Xahau/xahau.js.git
synced 2025-12-01 17:45:49 +00:00
feat: amm devnet to fundWallet and custom faucetPaths (#2083)
* Add amm devnet support * Add option for faucet paths Co-authored-by: Jackson Mills <aim4math@gmail.com>
This commit is contained in:
@@ -11,6 +11,8 @@ Subscribe to [the **xrpl-announce** mailing list](https://groups.google.com/g/xr
|
||||
|
||||
### Changed
|
||||
* Add support for Transaction objects in `verifyTransaction`
|
||||
* When connected to amm devnet, Client.fundWallet now defaults to using the faucet instead of requiring specification.
|
||||
* Ability to specify faucet url for wallet generation/funding purposes
|
||||
|
||||
## 2.5.0 (2022-10-13)
|
||||
### Added
|
||||
|
||||
70
packages/xrpl/src/Wallet/defaultFaucets.ts
Normal file
70
packages/xrpl/src/Wallet/defaultFaucets.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import type { Client } from '..'
|
||||
import { XRPLFaucetError } from '../errors'
|
||||
|
||||
export interface FaucetWallet {
|
||||
account: {
|
||||
xAddress: string
|
||||
classicAddress?: string
|
||||
secret: string
|
||||
}
|
||||
amount: number
|
||||
balance: number
|
||||
}
|
||||
|
||||
export enum FaucetNetwork {
|
||||
Testnet = 'faucet.altnet.rippletest.net',
|
||||
Devnet = 'faucet.devnet.rippletest.net',
|
||||
AMMDevnet = 'ammfaucet.devnet.rippletest.net',
|
||||
NFTDevnet = 'faucet-nft.ripple.com',
|
||||
}
|
||||
|
||||
export const FaucetNetworkPaths: Record<string, string> = {
|
||||
[FaucetNetwork.Testnet]: '/accounts',
|
||||
[FaucetNetwork.Devnet]: '/accounts',
|
||||
[FaucetNetwork.AMMDevnet]: '/accounts',
|
||||
[FaucetNetwork.NFTDevnet]: '/accounts',
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the faucet host based on the Client connection.
|
||||
*
|
||||
* @param client - Client.
|
||||
* @returns A {@link FaucetNetwork}.
|
||||
* @throws When the client url is not on altnet or devnet.
|
||||
*/
|
||||
export function getFaucetHost(client: Client): FaucetNetwork | undefined {
|
||||
const connectionUrl = client.url
|
||||
|
||||
// 'altnet' for Ripple Testnet server and 'testnet' for XRPL Labs Testnet server
|
||||
if (connectionUrl.includes('altnet') || connectionUrl.includes('testnet')) {
|
||||
return FaucetNetwork.Testnet
|
||||
}
|
||||
|
||||
if (connectionUrl.includes('amm')) {
|
||||
return FaucetNetwork.AMMDevnet
|
||||
}
|
||||
|
||||
if (connectionUrl.includes('devnet')) {
|
||||
return FaucetNetwork.Devnet
|
||||
}
|
||||
|
||||
if (connectionUrl.includes('xls20-sandbox')) {
|
||||
return FaucetNetwork.NFTDevnet
|
||||
}
|
||||
|
||||
throw new XRPLFaucetError('Faucet URL is not defined or inferrable.')
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the faucet pathname based on the faucet hostname.
|
||||
*
|
||||
* @param hostname - hostname.
|
||||
* @returns A String with the correct path for the input hostname.
|
||||
* If hostname undefined or cannot find (key, value) pair in {@link FaucetNetworkPaths}, defaults to '/accounts'
|
||||
*/
|
||||
export function getDefaultFaucetPath(hostname: string | undefined): string {
|
||||
if (hostname === undefined) {
|
||||
return '/accounts'
|
||||
}
|
||||
return FaucetNetworkPaths[hostname] || '/accounts'
|
||||
}
|
||||
@@ -6,24 +6,14 @@ import { isValidClassicAddress } from 'ripple-address-codec'
|
||||
import type { Client } from '..'
|
||||
import { RippledError, XRPLFaucetError } from '../errors'
|
||||
|
||||
import {
|
||||
FaucetWallet,
|
||||
getFaucetHost,
|
||||
getDefaultFaucetPath,
|
||||
} from './defaultFaucets'
|
||||
|
||||
import Wallet from '.'
|
||||
|
||||
interface FaucetWallet {
|
||||
account: {
|
||||
xAddress: string
|
||||
classicAddress?: string
|
||||
secret: string
|
||||
}
|
||||
amount: number
|
||||
balance: number
|
||||
}
|
||||
|
||||
enum FaucetNetwork {
|
||||
Testnet = 'faucet.altnet.rippletest.net',
|
||||
Devnet = 'faucet.devnet.rippletest.net',
|
||||
NFTDevnet = 'faucet-nft.ripple.com',
|
||||
}
|
||||
|
||||
// Interval to check an account balance
|
||||
const INTERVAL_SECONDS = 1
|
||||
// Maximum attempts to retrieve a balance
|
||||
@@ -42,22 +32,30 @@ const MAX_ATTEMPTS = 20
|
||||
* @param this - Client.
|
||||
* @param wallet - An existing XRPL Wallet to fund. If undefined or null, a new Wallet will be created.
|
||||
* @param options - See below.
|
||||
* @param options.faucetHost - A custom host for a faucet server. On devnet and
|
||||
* testnet, `fundWallet` will attempt to determine the correct server
|
||||
* automatically. In other environments, or if you would like to customize the
|
||||
* faucet host in devnet or testnet, you should provide the host using this
|
||||
* option.
|
||||
* @param options.faucetHost - A custom host for a faucet server. On devnet,
|
||||
* testnet, AMM devnet, NFT devnet testnet, `fundWallet` will
|
||||
* attempt to determine the correct server automatically. In other environments,
|
||||
* or if you would like to customize the faucet host in devnet or testnet,
|
||||
* you should provide the host using this option.
|
||||
* @param options.faucetPath - A custom path for a faucet server. On devnet,
|
||||
* testnet, AMM devnet, NFT devnet testnet, `fundWallet` will
|
||||
* attempt to determine the correct path automatically. In other environments,
|
||||
* or if you would like to customize the faucet path in devnet or testnet,
|
||||
* you should provide the path using this option.
|
||||
* Ex: client.fundWallet(null,{'faucet.altnet.rippletest.net', '/accounts'})
|
||||
* specifies a request to 'faucet.altnet.rippletest.net/accounts' to fund a new wallet.
|
||||
* @param options.amount - A custom amount to fund, if undefined or null, the default amount will be 1000.
|
||||
* @returns A Wallet on the Testnet or Devnet that contains some amount of XRP,
|
||||
* and that wallet's balance in XRP.
|
||||
* @throws When either Client isn't connected or unable to fund wallet address.
|
||||
*/
|
||||
// eslint-disable-next-line max-lines-per-function -- this function needs to display and do with more information.
|
||||
// eslint-disable-next-line max-lines-per-function -- All lines necessary
|
||||
async function fundWallet(
|
||||
this: Client,
|
||||
wallet?: Wallet | null,
|
||||
options?: {
|
||||
faucetHost?: string
|
||||
faucetPath?: string
|
||||
amount?: string
|
||||
},
|
||||
): Promise<{
|
||||
@@ -92,9 +90,11 @@ async function fundWallet(
|
||||
} catch {
|
||||
/* startingBalance remains '0' */
|
||||
}
|
||||
|
||||
// Options to pass to https.request
|
||||
const httpOptions = getHTTPOptions(this, postBody, options?.faucetHost)
|
||||
const httpOptions = getHTTPOptions(this, postBody, {
|
||||
hostname: options?.faucetHost,
|
||||
pathname: options?.faucetPath,
|
||||
})
|
||||
|
||||
return returnPromise(
|
||||
httpOptions,
|
||||
@@ -147,12 +147,17 @@ async function returnPromise(
|
||||
function getHTTPOptions(
|
||||
client: Client,
|
||||
postBody: Uint8Array,
|
||||
hostname?: string,
|
||||
options?: {
|
||||
hostname?: string
|
||||
pathname?: string
|
||||
},
|
||||
): RequestOptions {
|
||||
const finalHostname = options?.hostname ?? getFaucetHost(client)
|
||||
const finalPathname = options?.pathname ?? getDefaultFaucetPath(finalHostname)
|
||||
return {
|
||||
hostname: hostname ?? getFaucetHost(client),
|
||||
hostname: finalHostname,
|
||||
port: 443,
|
||||
path: '/accounts',
|
||||
path: finalPathname,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -175,11 +180,14 @@ async function onEnd(
|
||||
|
||||
// "application/json; charset=utf-8"
|
||||
if (response.headers['content-type']?.startsWith('application/json')) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- We know this is safe and correct
|
||||
const faucetWallet: FaucetWallet = JSON.parse(body)
|
||||
const classicAddress = faucetWallet.account.classicAddress
|
||||
await processSuccessfulResponse(
|
||||
client,
|
||||
body,
|
||||
startingBalance,
|
||||
classicAddress,
|
||||
walletToFund,
|
||||
startingBalance,
|
||||
resolve,
|
||||
reject,
|
||||
)
|
||||
@@ -199,16 +207,12 @@ async function onEnd(
|
||||
// eslint-disable-next-line max-params, max-lines-per-function -- Only used as a helper function, lines inc due to added balance.
|
||||
async function processSuccessfulResponse(
|
||||
client: Client,
|
||||
body: string,
|
||||
startingBalance: number,
|
||||
classicAddress: string | undefined,
|
||||
walletToFund: Wallet,
|
||||
startingBalance: number,
|
||||
resolve: (response: { wallet: Wallet; balance: number }) => void,
|
||||
reject: (err: ErrorConstructor | Error | unknown) => void,
|
||||
): Promise<void> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- We know this is safe and correct
|
||||
const faucetWallet: FaucetWallet = JSON.parse(body)
|
||||
const classicAddress = faucetWallet.account.classicAddress
|
||||
|
||||
if (!classicAddress) {
|
||||
reject(new XRPLFaucetError(`The faucet account is undefined`))
|
||||
return
|
||||
@@ -298,37 +302,4 @@ async function getUpdatedBalance(
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the faucet host based on the Client connection.
|
||||
*
|
||||
* @param client - Client.
|
||||
* @returns A {@link FaucetNetwork}.
|
||||
* @throws When the client url is not on altnet or devnet.
|
||||
*/
|
||||
function getFaucetHost(client: Client): FaucetNetwork | undefined {
|
||||
const connectionUrl = client.url
|
||||
|
||||
// 'altnet' for Ripple Testnet server and 'testnet' for XRPL Labs Testnet server
|
||||
if (connectionUrl.includes('altnet') || connectionUrl.includes('testnet')) {
|
||||
return FaucetNetwork.Testnet
|
||||
}
|
||||
|
||||
if (connectionUrl.includes('devnet')) {
|
||||
return FaucetNetwork.Devnet
|
||||
}
|
||||
|
||||
if (connectionUrl.includes('xls20-sandbox')) {
|
||||
return FaucetNetwork.NFTDevnet
|
||||
}
|
||||
|
||||
throw new XRPLFaucetError('Faucet URL is not defined or inferrable.')
|
||||
}
|
||||
|
||||
export default fundWallet
|
||||
|
||||
const _private = {
|
||||
FaucetNetwork,
|
||||
getFaucetHost,
|
||||
}
|
||||
|
||||
export { _private }
|
||||
|
||||
@@ -4,7 +4,7 @@ import path from 'path'
|
||||
import { expect, assert } from 'chai'
|
||||
import puppeteer from 'puppeteer'
|
||||
|
||||
const TIMEOUT = 80000
|
||||
const TIMEOUT = 150000
|
||||
interface TestCaseInfo {
|
||||
name: string
|
||||
span: string
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
isValidXAddress,
|
||||
dropsToXrp,
|
||||
} from 'xrpl-local'
|
||||
|
||||
// how long before each test case times out
|
||||
const TIMEOUT = 60000
|
||||
// This test is reliant on external networks, and as such may be flaky.
|
||||
@@ -14,121 +15,37 @@ describe('fundWallet', function () {
|
||||
this.timeout(TIMEOUT)
|
||||
|
||||
it('submit generates a testnet wallet', async function () {
|
||||
const api = new Client('wss://s.altnet.rippletest.net:51233')
|
||||
|
||||
await api.connect()
|
||||
const { wallet, balance } = await api.fundWallet()
|
||||
|
||||
assert.notEqual(wallet, undefined)
|
||||
assert(isValidClassicAddress(wallet.classicAddress))
|
||||
assert(isValidXAddress(wallet.getXAddress()))
|
||||
|
||||
const info = await api.request({
|
||||
command: 'account_info',
|
||||
account: wallet.classicAddress,
|
||||
})
|
||||
assert.equal(dropsToXrp(info.result.account_data.Balance), balance)
|
||||
|
||||
const { balance: newBalance } = await api.fundWallet(wallet)
|
||||
|
||||
const afterSent = await api.request({
|
||||
command: 'account_info',
|
||||
account: wallet.classicAddress,
|
||||
})
|
||||
|
||||
assert.equal(dropsToXrp(afterSent.result.account_data.Balance), newBalance)
|
||||
|
||||
await api.disconnect()
|
||||
await generate_faucet_wallet_and_fund_again(
|
||||
'wss://s.altnet.rippletest.net:51233',
|
||||
)
|
||||
})
|
||||
|
||||
it('submit generates a devnet wallet', async function () {
|
||||
const api = new Client('wss://s.devnet.rippletest.net:51233')
|
||||
|
||||
await api.connect()
|
||||
const { wallet, balance } = await api.fundWallet()
|
||||
|
||||
assert.notEqual(wallet, undefined)
|
||||
assert(isValidClassicAddress(wallet.classicAddress))
|
||||
assert(isValidXAddress(wallet.getXAddress()))
|
||||
|
||||
const info = await api.request({
|
||||
command: 'account_info',
|
||||
account: wallet.classicAddress,
|
||||
})
|
||||
|
||||
assert.equal(dropsToXrp(info.result.account_data.Balance), balance)
|
||||
|
||||
const { balance: newBalance } = await api.fundWallet(wallet)
|
||||
|
||||
const afterSent = await api.request({
|
||||
command: 'account_info',
|
||||
account: wallet.classicAddress,
|
||||
})
|
||||
assert.equal(dropsToXrp(afterSent.result.account_data.Balance), newBalance)
|
||||
|
||||
await api.disconnect()
|
||||
await generate_faucet_wallet_and_fund_again(
|
||||
'wss://s.devnet.rippletest.net:51233',
|
||||
)
|
||||
})
|
||||
|
||||
it('can generate and fund wallets on nft-devnet', async function () {
|
||||
const api = new Client('ws://xls20-sandbox.rippletest.net:51233')
|
||||
|
||||
await api.connect()
|
||||
const { wallet, balance } = await api.fundWallet()
|
||||
assert.notEqual(wallet, undefined)
|
||||
assert(isValidClassicAddress(wallet.classicAddress))
|
||||
assert(isValidXAddress(wallet.getXAddress()))
|
||||
|
||||
const info = await api.request({
|
||||
command: 'account_info',
|
||||
account: wallet.classicAddress,
|
||||
})
|
||||
|
||||
assert.equal(dropsToXrp(info.result.account_data.Balance), balance)
|
||||
|
||||
const { balance: newBalance } = await api.fundWallet(wallet, {
|
||||
faucetHost: 'faucet-nft.ripple.com',
|
||||
})
|
||||
|
||||
const afterSent = await api.request({
|
||||
command: 'account_info',
|
||||
account: wallet.classicAddress,
|
||||
})
|
||||
assert.equal(dropsToXrp(afterSent.result.account_data.Balance), newBalance)
|
||||
|
||||
await api.disconnect()
|
||||
await generate_faucet_wallet_and_fund_again(
|
||||
'ws://xls20-sandbox.rippletest.net:51233',
|
||||
)
|
||||
})
|
||||
|
||||
it('can generate and fund wallets using a custom host', async function () {
|
||||
const api = new Client('ws://xls20-sandbox.rippletest.net:51233')
|
||||
|
||||
await api.connect()
|
||||
const { wallet, balance } = await api.fundWallet(null, {
|
||||
faucetHost: 'faucet-nft.ripple.com',
|
||||
})
|
||||
|
||||
assert.notEqual(wallet, undefined)
|
||||
assert(isValidClassicAddress(wallet.classicAddress))
|
||||
assert(isValidXAddress(wallet.getXAddress()))
|
||||
|
||||
const info = await api.request({
|
||||
command: 'account_info',
|
||||
account: wallet.classicAddress,
|
||||
})
|
||||
|
||||
assert.equal(dropsToXrp(info.result.account_data.Balance), balance)
|
||||
|
||||
const { balance: newBalance } = await api.fundWallet(wallet, {
|
||||
faucetHost: 'faucet-nft.ripple.com',
|
||||
})
|
||||
|
||||
const afterSent = await api.request({
|
||||
command: 'account_info',
|
||||
account: wallet.classicAddress,
|
||||
})
|
||||
assert.equal(dropsToXrp(afterSent.result.account_data.Balance), newBalance)
|
||||
|
||||
await api.disconnect()
|
||||
it('can generate and fund wallets using a custom host and path', async function () {
|
||||
await generate_faucet_wallet_and_fund_again(
|
||||
'ws://xls20-sandbox.rippletest.net:51233',
|
||||
'faucet-nft.ripple.com',
|
||||
'/accounts',
|
||||
)
|
||||
})
|
||||
|
||||
it('can generate and fund wallets on AMM devnet', async function () {
|
||||
await generate_faucet_wallet_and_fund_again(
|
||||
'wss://amm.devnet.rippletest.net:51233',
|
||||
)
|
||||
})
|
||||
|
||||
it('submit funds wallet with custom amount', async function () {
|
||||
const api = new Client('wss://s.altnet.rippletest.net:51233')
|
||||
|
||||
@@ -147,3 +64,43 @@ describe('fundWallet', function () {
|
||||
await api.disconnect()
|
||||
})
|
||||
})
|
||||
|
||||
async function generate_faucet_wallet_and_fund_again(
|
||||
client: string,
|
||||
faucetHost: string | undefined = undefined,
|
||||
faucetPath: string | undefined = undefined,
|
||||
): Promise<void> {
|
||||
const api = new Client(client)
|
||||
|
||||
await api.connect()
|
||||
|
||||
const { wallet, balance } = await api.fundWallet(null, {
|
||||
faucetHost,
|
||||
faucetPath,
|
||||
})
|
||||
assert.notEqual(wallet, undefined)
|
||||
assert(isValidClassicAddress(wallet.classicAddress))
|
||||
assert(isValidXAddress(wallet.getXAddress()))
|
||||
|
||||
const info = await api.request({
|
||||
command: 'account_info',
|
||||
account: wallet.classicAddress,
|
||||
})
|
||||
|
||||
assert.equal(dropsToXrp(info.result.account_data.Balance), balance)
|
||||
|
||||
const { balance: newBalance } = await api.fundWallet(wallet, {
|
||||
faucetHost,
|
||||
faucetPath,
|
||||
})
|
||||
|
||||
const afterSent = await api.request({
|
||||
command: 'account_info',
|
||||
account: wallet.classicAddress,
|
||||
})
|
||||
assert.equal(dropsToXrp(afterSent.result.account_data.Balance), newBalance)
|
||||
|
||||
assert(newBalance > balance)
|
||||
|
||||
await api.disconnect()
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { assert } from 'chai'
|
||||
|
||||
import { _private } from '../../src/Wallet/fundWallet'
|
||||
import {
|
||||
FaucetNetwork,
|
||||
FaucetNetworkPaths,
|
||||
getFaucetHost,
|
||||
getDefaultFaucetPath,
|
||||
} from '../../src/Wallet/defaultFaucets'
|
||||
import { setupClient, teardownClient } from '../setupClient'
|
||||
|
||||
const { FaucetNetwork, getFaucetHost } = _private
|
||||
|
||||
describe('Get Faucet host ', function () {
|
||||
beforeEach(setupClient)
|
||||
afterEach(teardownClient)
|
||||
@@ -30,7 +33,30 @@ describe('Get Faucet host ', function () {
|
||||
assert.strictEqual(getFaucetHost(this.client), expectedFaucet)
|
||||
})
|
||||
|
||||
it('returns undefined if not a Testnet or Devnet server URL', function () {
|
||||
it('returns the NFT-Devnet host with the XLS-20 Sandbox server', function () {
|
||||
const expectedFaucet = FaucetNetwork.NFTDevnet
|
||||
this.client.connection.url = 'ws://xls20-sandbox.rippletest.net:51233'
|
||||
|
||||
assert.strictEqual(getFaucetHost(this.client), expectedFaucet)
|
||||
})
|
||||
|
||||
it('returns the correct faucetPath for Devnet host', function () {
|
||||
const expectedFaucetPath = FaucetNetworkPaths[FaucetNetwork.Devnet]
|
||||
this.client.connection.url = FaucetNetwork.Devnet
|
||||
|
||||
assert.strictEqual(
|
||||
getDefaultFaucetPath(getFaucetHost(this.client)),
|
||||
expectedFaucetPath,
|
||||
)
|
||||
})
|
||||
|
||||
it('returns the correct faucetPath for undefined host', function () {
|
||||
const expectedFaucetPath = '/accounts'
|
||||
|
||||
assert.strictEqual(getDefaultFaucetPath(undefined), expectedFaucetPath)
|
||||
})
|
||||
|
||||
it('throws if not connected to a known faucet host', function () {
|
||||
// Info: setupClient.setup creates a connection to 'localhost'
|
||||
assert.throws(() => getFaucetHost(this.client))
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user