@veil-cash/sdk 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veil-cash/sdk",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "SDK and CLI for interacting with Veil Cash privacy pools - keypair generation, deposits, and status checking",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,11 +1,13 @@
1
1
  ---
2
2
  name: veil
3
- version: 0.5.0
3
+ version: 0.6.2
4
4
  description: >
5
5
  Veil CLI for private ETH and USDC transactions on Base. Use when the user wants
6
6
  to deposit, withdraw, or transfer assets privately, check private balances,
7
- manage Veil keypairs, register on-chain, or build unsigned transaction payloads
8
- for an external signer (e.g. Bankr). All operations target Base (chain ID 8453).
7
+ manage Veil keypairs, register on-chain, manage deterministic subaccounts
8
+ (forwarder deploy, sweep, merge to main wallet, recover), or build unsigned
9
+ transaction payloads for an external signer (e.g. Bankr). All operations
10
+ target Base (chain ID 8453).
9
11
  author: veildotcash
10
12
  metadata:
11
13
  homepage: https://veil.cash
@@ -27,11 +29,16 @@ triggers:
27
29
  - pattern: veil withdraw
28
30
  - pattern: veil transfer
29
31
  - pattern: veil merge
32
+ - pattern: veil subaccount
30
33
  - pattern: unsigned payload
31
34
  - pattern: privacy pool
32
35
  - pattern: deposit privately
33
36
  - pattern: withdraw privately
34
37
  - pattern: private transfer
38
+ - pattern: subaccount
39
+ - pattern: subaccount merge
40
+ - pattern: forwarder
41
+ - pattern: stealth deposit
35
42
  ---
36
43
 
37
44
  # Veil CLI
@@ -164,6 +171,8 @@ What do you want to do?
164
171
  |
165
172
  +-- Withdraw / transfer / merge → Section 5
166
173
  |
174
+ +-- Subaccounts (forwarders) → Section 5B
175
+ |
167
176
  +-- Inspect or rotate keypair → veil keypair / veil init --force
168
177
  ```
169
178
 
@@ -188,6 +197,13 @@ What do you want to do?
188
197
  | Withdraw | `veil withdraw ETH 0.05 0xRecipient` |
189
198
  | Transfer privately | `veil transfer ETH 0.02 0xRecipient` |
190
199
  | Merge UTXOs | `veil merge ETH 0.1` |
200
+ | Derive subaccount | `veil subaccount derive --slot 0` |
201
+ | Subaccount status | `veil subaccount status --slot 0` |
202
+ | Subaccount address | `veil subaccount address --slot 0` |
203
+ | Deploy forwarder | `veil subaccount deploy --slot 0` |
204
+ | Sweep forwarder | `veil subaccount sweep --slot 0 --asset eth` |
205
+ | Merge subaccount to main | `veil subaccount merge --slot 0 --pool eth` |
206
+ | Recover from forwarder | `veil subaccount recover --slot 0 --asset usdc --to 0xAddr --amount 25` |
191
207
 
192
208
  ---
193
209
 
@@ -315,6 +331,8 @@ Important:
315
331
 
316
332
  Deposits treat the CLI amount as the **net** amount that lands in the pool.
317
333
  The `0.3%` protocol fee is calculated on-chain and added automatically.
334
+ After submission, deposits go through screening / queue processing before they
335
+ are accepted into the private pool. This typically takes around `10-15 minutes`.
318
336
 
319
337
  ```bash
320
338
  veil deposit ETH 0.1
@@ -369,6 +387,9 @@ Human-readable balance output includes:
369
387
  - wallet public balances (`ETH`, `USDC`)
370
388
  - queue and private balances
371
389
 
390
+ If a recent deposit still appears in queue balance, screening / queue processing
391
+ may still be in progress. Typical processing time is around `10-15 minutes`.
392
+
372
393
  ---
373
394
 
374
395
  ## 5. Private Actions
@@ -403,6 +424,74 @@ Note: withdraw proof generation is single-threaded for reliable CLI exit after s
403
424
 
404
425
  ---
405
426
 
427
+ ## 5B. Subaccounts
428
+
429
+ Subaccounts are deterministic child slots derived from your main `VEIL_KEY`:
430
+
431
+ `root key → slot → child key → child deposit key → forwarder`
432
+
433
+ Base mainnet only. Slots are `0`–`2` (max 3 subaccounts). Deploy and sweep are
434
+ relay-backed (no `WALLET_KEY` needed). Merge transfers the subaccount's private
435
+ pool balance back to the main wallet via a ZK proof (relay-backed, no `WALLET_KEY`
436
+ needed). Recovery submits a direct on-chain transaction and **requires `WALLET_KEY`**
437
+ as a gas payer.
438
+
439
+ Status reports the child slot's forwarder wallet balances, private pool
440
+ balances, and queue state.
441
+
442
+ ### Derive and inspect
443
+
444
+ ```bash
445
+ veil subaccount derive --slot 0 # Full slot metadata
446
+ veil subaccount derive --slot 0 --json
447
+ veil subaccount address --slot 0 # Just the forwarder address
448
+ veil subaccount status --slot 0 # Deployment, forwarder balances, private balances, queue state
449
+ veil subaccount status --slot 0 --json
450
+ ```
451
+
452
+ ### Deploy and sweep (relay-backed)
453
+
454
+ ```bash
455
+ veil subaccount deploy --slot 0 # Deploy the forwarder contract
456
+ veil subaccount deploy --slot 0 --json
457
+ veil subaccount sweep --slot 0 --asset eth # Sweep ETH into the pool
458
+ veil subaccount sweep --slot 0 --asset usdc # Sweep USDC into the pool
459
+ veil subaccount sweep --slot 0 --asset eth --json
460
+ ```
461
+
462
+ ### Merge subaccount to main wallet (relay-backed)
463
+
464
+ Merge transfers the subaccount's entire private pool balance back to the main
465
+ wallet. It builds a ZK proof transferring child UTXOs to the parent keypair and
466
+ submits via the relay. Only needs `VEIL_KEY`.
467
+
468
+ ```bash
469
+ veil subaccount merge --slot 0 --pool eth
470
+ veil subaccount merge --slot 0 --pool usdc
471
+ veil subaccount merge --slot 0 --pool eth --json
472
+ ```
473
+
474
+ ### Recover (direct on-chain — requires WALLET_KEY)
475
+
476
+ Recovery is for assets still sitting on the forwarder after refund or rejection.
477
+ It signs a forwarder withdraw with the child key and submits the transaction
478
+ using `WALLET_KEY` as the gas payer.
479
+
480
+ ```bash
481
+ veil subaccount recover --slot 0 --asset usdc --to 0xRecipient --amount 25
482
+ veil subaccount recover --slot 0 --asset eth --to 0xRecipient --amount 0.05 --json
483
+ ```
484
+
485
+ Important:
486
+
487
+ - `--asset` is `eth` or `usdc` (case-insensitive in the CLI)
488
+ - `--slot` is `0`–`2`
489
+ - Deploy and sweep only need `VEIL_KEY`
490
+ - Merge only needs `VEIL_KEY`
491
+ - Recover needs both `VEIL_KEY` and `WALLET_KEY`
492
+
493
+ ---
494
+
406
495
  ## 6. Unsigned Payloads
407
496
 
408
497
  `--unsigned` is for external signer workflows. The CLI emits a signer-compatible
@@ -504,6 +593,7 @@ All CLI errors output JSON with a standardised `errorCode`:
504
593
  | `DEPOSIT_KEY_MISSING` | `DEPOSIT_KEY` missing from `.env.veil` | Re-run `veil init` to regenerate |
505
594
  | `USER_NOT_REGISTERED` | Transfer recipient has no deposit key registered on-chain | Recipient must run `veil register` first |
506
595
  | `INVALID_AMOUNT` | Amount below minimum or invalid format | ETH min: `0.01`, USDC min: `10` |
596
+ | `INVALID_SLOT` | Invalid subaccount slot | Slot must be `0`–`2` (non-negative integer) |
507
597
  | `INSUFFICIENT_BALANCE` | Not enough ETH for gas | Top up Base ETH balance |
508
598
  | `RPC_ERROR` | Network or RPC failure | Check `RPC_URL` env var or retry |
509
599
  | `RELAY_ERROR` | Relayer rejected the proof | Check relay health with `veil status`; retry |
@@ -143,6 +143,81 @@ const priv = await getPrivateBalance({
143
143
  });
144
144
  ```
145
145
 
146
+ ### Subaccounts
147
+
148
+ ```typescript
149
+ import {
150
+ deriveSubaccountSlot,
151
+ getSubaccountPrivateBalance,
152
+ getSubaccountStatus,
153
+ deploySubaccountForwarder,
154
+ sweepSubaccountForwarder,
155
+ mergeSubaccount,
156
+ buildSubaccountRecoveryTx,
157
+ isSubaccountForwarderDeployed,
158
+ MAX_SUBACCOUNT_SLOTS,
159
+ } from '@veil-cash/sdk';
160
+
161
+ // Derive slot metadata (child key, salt, predicted forwarder address)
162
+ const slot = await deriveSubaccountSlot({
163
+ rootPrivateKey: '0xVEIL_KEY',
164
+ slot: 0, // 0–2
165
+ });
166
+ // slot.forwarderAddress, slot.childOwner, slot.childDepositKey, slot.salt
167
+
168
+ // Check deployment status
169
+ const deployed = await isSubaccountForwarderDeployed({
170
+ forwarderAddress: slot.forwarderAddress,
171
+ });
172
+
173
+ // Full status (deployment, forwarder balances, private balances, queue state)
174
+ const status = await getSubaccountStatus({
175
+ rootPrivateKey: '0xVEIL_KEY',
176
+ slot: 0,
177
+ });
178
+ // status.deployed, status.balances, status.privateBalances, status.queues
179
+
180
+ // Private pool balance for a single slot + pool
181
+ const privateBalance = await getSubaccountPrivateBalance({
182
+ rootPrivateKey: '0xVEIL_KEY',
183
+ slot: 0,
184
+ pool: 'eth',
185
+ });
186
+ // privateBalance.privateBalance, privateBalance.unspentCount, privateBalance.utxos
187
+
188
+ // Deploy forwarder (relay-backed, no WALLET_KEY needed)
189
+ const deployResult = await deploySubaccountForwarder({
190
+ rootPrivateKey: '0xVEIL_KEY',
191
+ slot: 0,
192
+ });
193
+ // deployResult.transactionHash, deployResult.slot.forwarderAddress
194
+
195
+ // Sweep assets into pool (relay-backed)
196
+ const sweepResult = await sweepSubaccountForwarder({
197
+ forwarderAddress: slot.forwarderAddress,
198
+ asset: 'eth', // 'eth' | 'usdc'
199
+ });
200
+
201
+ // Merge subaccount's private balance back to main wallet (relay-backed)
202
+ const mergeResult = await mergeSubaccount({
203
+ rootPrivateKey: '0xVEIL_KEY',
204
+ slot: 0,
205
+ pool: 'eth', // 'eth' | 'usdc' (default: 'eth')
206
+ });
207
+ // mergeResult.success, mergeResult.transactionHash, mergeResult.amount, mergeResult.slot, mergeResult.pool
208
+
209
+ // Build recovery transaction (for assets stuck on forwarder)
210
+ const recovery = await buildSubaccountRecoveryTx({
211
+ rootPrivateKey: '0xVEIL_KEY',
212
+ slot: 0,
213
+ asset: 'usdc',
214
+ to: '0xRecipient',
215
+ amount: '25',
216
+ });
217
+ // recovery.transaction — submit with your wallet client
218
+ // recovery.forwarderAddress, recovery.signature, recovery.nonce, recovery.deadline
219
+ ```
220
+
146
221
  ---
147
222
 
148
223
  ## CLI quick reference
@@ -158,7 +233,7 @@ Install globally: `npm install -g @veil-cash/sdk`
158
233
  | `WALLET_KEY` | Ethereum wallet private key (for signing) |
159
234
  | `SIGNER_ADDRESS` | Ethereum address for unsigned/query flows when signing is external |
160
235
  | `RPC_URL` | Base RPC URL (optional, defaults to public RPC) |
161
- | `RELAY_URL` | Override relay base URL for relayed CLI operations |
236
+ | `RELAY_URL` | Override relay base URL for relayed CLI operations, subaccount deploy/sweep, and status checks |
162
237
 
163
238
  `WALLET_KEY` and `SIGNER_ADDRESS` are mutually exclusive. Use `SIGNER_ADDRESS` only for address-only CLI flows.
164
239
 
@@ -195,6 +270,20 @@ veil balance queue --pool eth # Queue-only balance
195
270
  veil balance queue --address 0x... --json # Queue balance for explicit address
196
271
  veil balance private --pool eth # Private-only balance
197
272
  veil balance private --json # Private balance as JSON
273
+
274
+ veil subaccount derive --slot 0 # Derive slot metadata
275
+ veil subaccount derive --slot 0 --json # Derive as JSON
276
+ veil subaccount address --slot 0 # Print forwarder address
277
+ veil subaccount status --slot 0 # Deployment, forwarder balances, private balances, queue state
278
+ veil subaccount status --slot 0 --json # Status as JSON
279
+ veil subaccount deploy --slot 0 # Deploy forwarder (relay-backed)
280
+ veil subaccount deploy --slot 0 --json # Deploy as JSON
281
+ veil subaccount sweep --slot 0 --asset eth # Sweep ETH into pool (relay-backed)
282
+ veil subaccount sweep --slot 0 --asset usdc --json # Sweep USDC as JSON
283
+ veil subaccount merge --slot 0 --pool eth # Merge subaccount balance to main wallet
284
+ veil subaccount merge --slot 0 --pool usdc --json # Merge USDC as JSON
285
+ veil subaccount recover --slot 0 --asset usdc --to 0x... --amount 25 # Recover assets (needs WALLET_KEY)
286
+ veil subaccount recover --slot 0 --asset eth --to 0x... --amount 0.05 --json
198
287
  ```
199
288
 
200
289
  ### Error format
@@ -210,7 +299,7 @@ All CLI errors output JSON with a standardized `errorCode`:
210
299
  ```
211
300
 
212
301
  Common codes: `VEIL_KEY_MISSING`, `WALLET_KEY_MISSING`, `DEPOSIT_KEY_MISSING`,
213
- `CONFIG_CONFLICT`, `INVALID_AMOUNT`, `INSUFFICIENT_BALANCE`, `CONTRACT_ERROR`, `RPC_ERROR`.
302
+ `CONFIG_CONFLICT`, `INVALID_AMOUNT`, `INVALID_SLOT`, `INSUFFICIENT_BALANCE`, `CONTRACT_ERROR`, `RPC_ERROR`.
214
303
 
215
304
  ---
216
305
 
@@ -7,13 +7,14 @@ import {
7
7
  getSubaccountStatus,
8
8
  isSubaccountForwarderDeployed,
9
9
  MAX_SUBACCOUNT_SLOTS,
10
+ mergeSubaccount,
10
11
  sweepSubaccountForwarder,
11
12
  } from '../../subaccount.js';
12
13
  import { getConfig } from '../config.js';
13
14
  import { CLIError, ErrorCode, handleCLIError } from '../errors.js';
14
15
  import { printFields, printHeader, printJson, printLine, printList, printSection, txUrl } from '../output.js';
15
16
  import { sendTransaction } from '../wallet.js';
16
- import type { SubaccountAsset } from '../../types.js';
17
+ import type { SubaccountAsset, RelayPool } from '../../types.js';
17
18
 
18
19
  function parseSlotValue(raw: string): number {
19
20
  const normalized = raw.trim();
@@ -51,6 +52,14 @@ function parseAsset(raw: string): SubaccountAsset {
51
52
  return asset;
52
53
  }
53
54
 
55
+ function parsePool(raw: string): RelayPool {
56
+ const pool = raw.toLowerCase();
57
+ if (pool !== 'eth' && pool !== 'usdc') {
58
+ throw new CLIError(ErrorCode.INVALID_AMOUNT, `Unsupported pool: ${raw}. Supported: eth, usdc`);
59
+ }
60
+ return pool;
61
+ }
62
+
54
63
  function printQueueHuman(
55
64
  title: string,
56
65
  queue: {
@@ -81,6 +90,7 @@ Examples:
81
90
  veil subaccount status --slot 0
82
91
  veil subaccount deploy --slot 0
83
92
  veil subaccount sweep --slot 0 --asset eth
93
+ veil subaccount merge --slot 0 --pool eth
84
94
  veil subaccount recover --slot 0 --asset usdc --to 0xRecipientAddress --amount 25
85
95
  veil subaccount address --slot 0
86
96
  `);
@@ -130,7 +140,7 @@ Examples:
130
140
 
131
141
  subaccount
132
142
  .command('status')
133
- .description('Show subaccount deployment, balances, and queue state')
143
+ .description('Show subaccount deployment, forwarder balances, private balances, and queue state')
134
144
  .requiredOption('--slot <n>', 'Subaccount slot', parseSlotValue)
135
145
  .option('--json', 'Output as JSON')
136
146
  .action(async (options) => {
@@ -162,6 +172,18 @@ Examples:
162
172
  { label: 'USDC', value: `${status.balances.usdc.balance} USDC` },
163
173
  ]);
164
174
 
175
+ printSection('Private Pool Balances');
176
+ printFields([
177
+ {
178
+ label: 'ETH',
179
+ value: `${status.privateBalances.eth.privateBalance} ETH (${status.privateBalances.eth.unspentCount} unspent / ${status.privateBalances.eth.spentCount} spent / ${status.privateBalances.eth.utxoCount} total UTXOs)`,
180
+ },
181
+ {
182
+ label: 'USDC',
183
+ value: `${status.privateBalances.usdc.privateBalance} USDC (${status.privateBalances.usdc.unspentCount} unspent / ${status.privateBalances.usdc.spentCount} spent / ${status.privateBalances.usdc.utxoCount} total UTXOs)`,
184
+ },
185
+ ]);
186
+
165
187
  printQueueHuman('ETH Queue', status.queues.eth);
166
188
  printQueueHuman('USDC Queue', status.queues.usdc);
167
189
  printLine();
@@ -178,11 +200,6 @@ Examples:
178
200
  .action(async (options) => {
179
201
  try {
180
202
  const rootPrivateKey = getRequiredVeilKey();
181
- const slot = await deriveSubaccountSlot({
182
- rootPrivateKey,
183
- slot: options.slot,
184
- rpcUrl: process.env.RPC_URL,
185
- });
186
203
  const result = await deploySubaccountForwarder({
187
204
  rootPrivateKey,
188
205
  slot: options.slot,
@@ -193,7 +210,7 @@ Examples:
193
210
  const output = {
194
211
  ...result,
195
212
  slot: options.slot,
196
- forwarderAddress: slot.forwarderAddress,
213
+ forwarderAddress: result.slot.forwarderAddress,
197
214
  };
198
215
 
199
216
  if (options.json) {
@@ -204,7 +221,7 @@ Examples:
204
221
  printHeader('Subaccount Deploy Submitted');
205
222
  printFields([
206
223
  { label: 'Slot', value: options.slot },
207
- { label: 'Forwarder', value: slot.forwarderAddress },
224
+ { label: 'Forwarder', value: result.slot.forwarderAddress },
208
225
  { label: 'Transaction', value: txUrl(result.transactionHash) },
209
226
  { label: 'Block', value: result.blockNumber },
210
227
  ]);
@@ -260,6 +277,64 @@ Examples:
260
277
  }
261
278
  });
262
279
 
280
+ subaccount
281
+ .command('merge')
282
+ .description('Merge a subaccount\'s private pool balance back to the main wallet')
283
+ .requiredOption('--slot <n>', 'Subaccount slot', parseSlotValue)
284
+ .option('--pool <pool>', 'Pool to merge (eth or usdc)', parsePool, 'eth' as RelayPool)
285
+ .option('--json', 'Output as JSON')
286
+ .action(async (options) => {
287
+ try {
288
+ const rootPrivateKey = getRequiredVeilKey();
289
+ const result = await mergeSubaccount({
290
+ rootPrivateKey,
291
+ slot: options.slot,
292
+ pool: options.pool,
293
+ rpcUrl: process.env.RPC_URL,
294
+ relayUrl: process.env.RELAY_URL,
295
+ onProgress: options.json
296
+ ? undefined
297
+ : (stage, detail) => {
298
+ const msg = detail ? `${stage} ${detail}` : stage;
299
+ process.stderr.write(`\r\x1b[K${msg}`);
300
+ },
301
+ });
302
+
303
+ if (!options.json) {
304
+ process.stderr.write('\r\x1b[K');
305
+ }
306
+
307
+ const output = {
308
+ success: result.success,
309
+ slot: result.slot,
310
+ pool: result.pool,
311
+ amount: result.amount,
312
+ transactionHash: result.transactionHash,
313
+ blockNumber: result.blockNumber,
314
+ };
315
+
316
+ if (options.json) {
317
+ printJson(output);
318
+ return;
319
+ }
320
+
321
+ printHeader('Subaccount Merge Submitted');
322
+ printFields([
323
+ { label: 'Slot', value: result.slot },
324
+ { label: 'Pool', value: result.pool.toUpperCase() },
325
+ { label: 'Amount', value: result.amount },
326
+ { label: 'Transaction', value: txUrl(result.transactionHash) },
327
+ { label: 'Block', value: result.blockNumber },
328
+ ]);
329
+ printLine();
330
+ } catch (error) {
331
+ if (!options.json) {
332
+ process.stderr.write('\r\x1b[K');
333
+ }
334
+ handleCLIError(error);
335
+ }
336
+ });
337
+
263
338
  subaccount
264
339
  .command('recover')
265
340
  .description('Recover assets sitting on the subaccount forwarder with a direct withdraw transaction')
@@ -274,6 +349,12 @@ Examples:
274
349
  if (!isAddress(options.to)) {
275
350
  throw new CLIError(ErrorCode.INVALID_ADDRESS, `Invalid recipient address: ${options.to}`);
276
351
  }
352
+ if (!process.env.WALLET_KEY) {
353
+ throw new CLIError(
354
+ ErrorCode.WALLET_KEY_MISSING,
355
+ 'WALLET_KEY required for recovery. Recovery submits a transaction on-chain and needs a gas payer.',
356
+ );
357
+ }
277
358
  const config = getConfig({});
278
359
  const recovery = await buildSubaccountRecoveryTx({
279
360
  rootPrivateKey,
package/src/cli/index.ts CHANGED
@@ -38,7 +38,7 @@ const program = new Command();
38
38
  program
39
39
  .name('veil')
40
40
  .description('CLI for Veil Cash privacy pools on Base')
41
- .version('0.6.0')
41
+ .version('0.6.2')
42
42
  .addHelpText('after', `
43
43
  Getting started:
44
44
  veil init
package/src/index.ts CHANGED
@@ -139,12 +139,14 @@ export {
139
139
  isSubaccountForwarderDeployed,
140
140
  deploySubaccountForwarder,
141
141
  sweepSubaccountForwarder,
142
+ getSubaccountPrivateBalance,
142
143
  getSubaccountStatus,
143
144
  buildSubaccountWithdrawTypedData,
144
145
  signSubaccountWithdraw,
145
146
  isSubaccountWithdrawNonceUsed,
146
147
  findNextSubaccountWithdrawNonce,
147
148
  buildSubaccountRecoveryTx,
149
+ mergeSubaccount,
148
150
  } from './subaccount.js';
149
151
 
150
152
  // Utilities
@@ -197,8 +199,12 @@ export type {
197
199
  SubaccountRelayResult,
198
200
  SubaccountAssetBalance,
199
201
  SubaccountBalances,
202
+ SubaccountPrivateBalanceStatus,
203
+ SubaccountPrivateBalances,
200
204
  SubaccountQueueStatus,
201
205
  SubaccountStatusResult,
202
206
  SubaccountWithdrawTypedData,
203
207
  SubaccountRecoveryResult,
208
+ SubaccountMergeOptions,
209
+ SubaccountMergeResult,
204
210
  } from './types.js';
package/src/relay.ts CHANGED
@@ -64,7 +64,16 @@ export async function postRelayJson<T>(
64
64
  body: JSON.stringify(body),
65
65
  });
66
66
 
67
- const data = await response.json();
67
+ const text = await response.text();
68
+ let data: unknown;
69
+ try {
70
+ data = JSON.parse(text);
71
+ } catch {
72
+ throw new RelayError(
73
+ `Relay returned non-JSON response (HTTP ${response.status})`,
74
+ response.status,
75
+ );
76
+ }
68
77
 
69
78
  if (!response.ok) {
70
79
  const errorData = data as RelayErrorResponse;