/
fundWallet.ts
283 lines (266 loc) · 8.81 KB
/
fundWallet.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
import fetch from 'cross-fetch'
import { isValidClassicAddress } from 'ripple-address-codec'
import type { Client } from '../client'
import { XRPLFaucetError } from '../errors'
import {
FaucetWallet,
getFaucetHost,
getDefaultFaucetPath,
} from './defaultFaucets'
import { Wallet } from '.'
// Interval to check an account balance
const INTERVAL_SECONDS = 1
// Maximum attempts to retrieve a balance
const MAX_ATTEMPTS = 20
export interface FundingOptions {
/**
* A custom amount to fund, if undefined or null, the default amount will be 1000.
*/
amount?: string
/**
* A custom host for a faucet server. On devnet, testnet, AMM devnet, and HooksV3 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.
*/
faucetHost?: string
/**
* A custom path for a faucet server. On devnet,
* testnet, AMM devnet, and HooksV3 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.
*/
faucetPath?: string
/**
* An optional field to indicate the use case context of the faucet transaction
* Ex: integration test, code snippets.
*/
usageContext?: string
}
/**
* Parameters to pass into a faucet request to fund an XRP account.
*/
export interface FaucetRequestBody {
/**
* The address to fund. If no address is provided the faucet will fund a random account.
*/
destination?: string
/**
* The total amount of XRP to fund the account with.
*/
xrpAmount?: string
/**
* An optional field to indicate the use case context of the faucet transaction
* Ex: integration test, code snippets.
*/
usageContext?: string
/**
* Information about the context of where the faucet is being called from.
* Ex: xrpl.js or xrpl-py
*/
userAgent: string
}
/**
* Generate a new wallet to fund if no existing wallet is provided or its address is invalid.
*
* @param wallet - Optional existing wallet.
* @returns The wallet to fund.
*/
export function generateWalletToFund(wallet?: Wallet | null): Wallet {
if (wallet && isValidClassicAddress(wallet.classicAddress)) {
return wallet
}
return Wallet.generate()
}
/**
* Get the starting balance of the wallet.
*
* @param client - The client object.
* @param classicAddress - The classic address of the wallet.
* @returns The starting balance.
*/
export async function getStartingBalance(
client: Client,
classicAddress: string,
): Promise<number> {
let startingBalance = 0
try {
startingBalance = Number(await client.getXrpBalance(classicAddress))
} catch {
// startingBalance remains '0'
}
return startingBalance
}
export interface FundWalletOptions {
faucetHost?: string
faucetPath?: string
amount?: string
usageContext?: string
}
/**
*
* Helper function to request funding from a faucet. Should not be called directly from outside the xrpl.js library.
*
* @param options - See below
* @param options.faucetHost - A custom host for a faucet server. On devnet,
* testnet, AMM devnet, and HooksV3 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, and HooksV3 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.
* @param client - A connection to the XRPL to send requests and transactions.
* @param startingBalance - The amount of XRP in the given walletToFund on ledger already.
* @param walletToFund - An existing XRPL Wallet to fund.
* @param postBody - The content to send the faucet to indicate which address to fund, how much to fund it, and
* where the request is coming from.
* @returns A promise that resolves to a funded wallet and the balance within it.
*/
// eslint-disable-next-line max-params -- Helper function created for organizational purposes
export async function requestFunding(
options: FundingOptions,
client: Client,
startingBalance: number,
walletToFund: Wallet,
postBody: FaucetRequestBody,
): Promise<{
wallet: Wallet
balance: number
}> {
const hostname = options.faucetHost ?? getFaucetHost(client)
if (!hostname) {
throw new XRPLFaucetError('No faucet hostname could be derived')
}
const pathname = options.faucetPath ?? getDefaultFaucetPath(hostname)
const response = await fetch(`https://${hostname}${pathname}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(postBody),
})
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- it can be anything
const body = await response.json()
if (
response.ok &&
response.headers.get('Content-Type')?.startsWith('application/json')
) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- It's a FaucetWallet
const classicAddress = (body as FaucetWallet).account.classicAddress
return processSuccessfulResponse(
client,
classicAddress,
walletToFund,
startingBalance,
)
}
return processError(response, body)
}
// eslint-disable-next-line max-params -- Only used as a helper function, lines inc due to added balance.
async function processSuccessfulResponse(
client: Client,
classicAddress: string | undefined,
walletToFund: Wallet,
startingBalance: number,
): Promise<{
wallet: Wallet
balance: number
}> {
if (!classicAddress) {
return Promise.reject(
new XRPLFaucetError(`The faucet account is undefined`),
)
}
try {
// Check at regular interval if the address is enabled on the XRPL and funded
const updatedBalance = await getUpdatedBalance(
client,
classicAddress,
startingBalance,
)
if (updatedBalance > startingBalance) {
return {
wallet: walletToFund,
balance: updatedBalance,
}
}
throw new XRPLFaucetError(
`Unable to fund address with faucet after waiting ${
INTERVAL_SECONDS * MAX_ATTEMPTS
} seconds`,
)
} catch (err) {
if (err instanceof Error) {
throw new XRPLFaucetError(err.message)
}
throw err
}
}
async function processError(response: Response, body): Promise<never> {
return Promise.reject(
new XRPLFaucetError(
`Request failed: ${JSON.stringify({
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- json response could be anything
body: body || {},
contentType: response.headers.get('Content-Type'),
statusCode: response.status,
})}`,
),
)
}
/**
* Check at regular interval if the address is enabled on the XRPL and funded.
*
* @param client - Client.
* @param address - The account address to check.
* @param originalBalance - The initial balance before the funding.
* @returns A Promise boolean.
*/
async function getUpdatedBalance(
client: Client,
address: string,
originalBalance: number,
): Promise<number> {
return new Promise((resolve, reject) => {
let attempts = MAX_ATTEMPTS
// eslint-disable-next-line @typescript-eslint/no-misused-promises -- Not actually misused here, different resolve
const interval = setInterval(async () => {
if (attempts < 0) {
clearInterval(interval)
resolve(originalBalance)
} else {
attempts -= 1
}
try {
let newBalance
try {
newBalance = Number(await client.getXrpBalance(address))
} catch {
/* newBalance remains undefined */
}
if (newBalance > originalBalance) {
clearInterval(interval)
resolve(newBalance)
}
} catch (err) {
clearInterval(interval)
if (err instanceof Error) {
reject(
new XRPLFaucetError(
`Unable to check if the address ${address} balance has increased. Error: ${err.message}`,
),
)
}
reject(err)
}
}, INTERVAL_SECONDS * 1000)
})
}