openzoo 0.49.16 → 0.49.18
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/README.md +8 -8
- package/bin/openzoo.js +0 -11
- package/lib/config.js +18 -28
- package/lib/demo.js +7 -10
- package/lib/evm.js +22 -19
- package/lib/grokui.mjs +13 -317
- package/lib/info.js +9 -10
- package/lib/mcp.js +1 -3
- package/lib/openclaw.js +38 -5
- package/lib/pay.js +74 -270
- package/lib/podagent.mjs +5 -16
- package/lib/proxy.js +4 -10
- package/lib/x402.js +145 -54
- package/package.json +2 -2
- package/lib/evmwrap.js +0 -163
- package/lib/subscription.js +0 -207
- package/lib/wrap.js +0 -332
- package/lib/xbot.js +0 -1576
- package/lib/xburner.js +0 -80
package/lib/wrap.js
DELETED
|
@@ -1,332 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Internal funding plumbing for Solana 402 rails.
|
|
3
|
-
*
|
|
4
|
-
* The zoo's Solana quotes settle in a NAV-wrapped Token-2022 mint issued by
|
|
5
|
-
* the wrap-nav program (FrSER…). Users only ever hold and send plain USDC or
|
|
6
|
-
* TOKEN (EVULoNF4DeMBN4dGiZiDfpiiTfNZgoCvXWWgaV3epump) —
|
|
7
|
-
* this module converts the exact amount needed, at payment time, inside the
|
|
8
|
-
* shim. Nothing here is user-facing; never surface wrapped tickers or mints
|
|
9
|
-
* in messages that reach the user.
|
|
10
|
-
*
|
|
11
|
-
* CLIENT BUILDER COPY — extra.acquire.steps / 402 help (WRAP_ACQUIRE_STEPS).
|
|
12
|
-
*
|
|
13
|
-
* Wrap ix has 9 accounts. 0x6a = 106 = NotEnoughAccounts, thrown at
|
|
14
|
-
* need(accounts, 9)?. Old 5-account Wrap is rejected.
|
|
15
|
-
*
|
|
16
|
-
* Program FrSERTNCPvTtaDS9AvQp9u1nYGzXDb3kC9MdL8Xxn2NE now CPIs the deposit.
|
|
17
|
-
* Delete the separate TransferChecked. Sending both double-transfers.
|
|
18
|
-
*
|
|
19
|
-
* data = [1] ++ u64 amount LE ++ [bump] (authority PDA bump, passed not derived)
|
|
20
|
-
*
|
|
21
|
-
* Accounts in exact order:
|
|
22
|
-
* 0 [writable] escrow (authority PDA ATA for UNDERLYING mint)
|
|
23
|
-
* 1 [writable] wrapped mint
|
|
24
|
-
* 2 [writable] recipient wrapped token account
|
|
25
|
-
* 3 [] wrapped mint authority PDA = PDA(["mint_authority", wrapped_mint], FrSER…)
|
|
26
|
-
* 4 [] wrapped token program (must equal wrapped_mint.owner; Token-2022 on shares)
|
|
27
|
-
* 5 [writable] depositor UNDERLYING token account
|
|
28
|
-
* 6 [signer] depositor (owner of account 5)
|
|
29
|
-
* 7 [] unwrapped mint
|
|
30
|
-
* 8 [] unwrapped token program (must equal escrow.owner)
|
|
31
|
-
*
|
|
32
|
-
* Account 4 and 8 are not interchangeable (LEOS: shares Token-2022, escrow
|
|
33
|
-
* TokenkegQ). Derive each ATA under its own token program.
|
|
34
|
-
* Prepend createAssociatedTokenAccountIdempotentInstruction for the recipient
|
|
35
|
-
* wrapped ATA. The program mints into it; it does not create it.
|
|
36
|
-
*
|
|
37
|
-
* Unwrap: 9 accounts, account 8 = unwrapped token program. 8 accounts → 0x70
|
|
38
|
-
* TokenProgramMismatch. Prepend idempotent ATA create for recipient underlying
|
|
39
|
-
* (may have been closed after wrapping).
|
|
40
|
-
*
|
|
41
|
-
* First deposit is 1:1 minus MINIMUM_LIQUIDITY (1000) locked forever.
|
|
42
|
-
* authority = PDA(['mint_authority', wrappedMint]); escrow = ATA(underlying,
|
|
43
|
-
* authority); registry = PDA(['backpointer', wrappedMint]) when present.
|
|
44
|
-
*/
|
|
45
|
-
import { PublicKey, Transaction, TransactionInstruction } from '@solana/web3.js';
|
|
46
|
-
import {
|
|
47
|
-
TOKEN_PROGRAM_ID,
|
|
48
|
-
TOKEN_2022_PROGRAM_ID,
|
|
49
|
-
createAssociatedTokenAccountIdempotentInstruction,
|
|
50
|
-
getAssociatedTokenAddressSync,
|
|
51
|
-
unpackAccount,
|
|
52
|
-
unpackMint,
|
|
53
|
-
} from '@solana/spl-token';
|
|
54
|
-
|
|
55
|
-
export const WRAP_PROGRAM_ID = new PublicKey('FrSERTNCPvTtaDS9AvQp9u1nYGzXDb3kC9MdL8Xxn2NE');
|
|
56
|
-
const AUTHORITY_SEED = Buffer.from('mint_authority');
|
|
57
|
-
const BACKPOINTER_SEED = Buffer.from('backpointer');
|
|
58
|
-
export const MINIMUM_LIQUIDITY = 1000n;
|
|
59
|
-
export const WRAP_IX_ACCOUNT_COUNT = 9;
|
|
60
|
-
export const WRAP_TOO_FEW_ACCOUNTS =
|
|
61
|
-
'wrap ix has too few accounts (need 9); old 5-account wrap is dead';
|
|
62
|
-
|
|
63
|
-
/** extra.acquire.steps / 402 help — the copy clients build the Wrap ix from. */
|
|
64
|
-
export const WRAP_ACQUIRE_STEPS = {
|
|
65
|
-
method: 'spl-token-wrap',
|
|
66
|
-
program: 'FrSERTNCPvTtaDS9AvQp9u1nYGzXDb3kC9MdL8Xxn2NE',
|
|
67
|
-
help: 'Wrap ix has 9 accounts. Program CPIs the deposit — do not send a separate TransferChecked. 0x6a = NotEnoughAccounts (old 5-account wrap is dead).',
|
|
68
|
-
data: '[1] ++ u64 amount LE ++ [bump] (authority PDA bump, passed not derived)',
|
|
69
|
-
prepend: 'createAssociatedTokenAccountIdempotentInstruction for the recipient wrapped ATA',
|
|
70
|
-
accounts: [
|
|
71
|
-
{ i: 0, writable: true, name: 'escrow', note: 'authority PDA ATA for UNDERLYING mint' },
|
|
72
|
-
{ i: 1, writable: true, name: 'wrappedMint' },
|
|
73
|
-
{ i: 2, writable: true, name: 'recipientWrappedAta' },
|
|
74
|
-
{ i: 3, writable: false, name: 'mintAuthorityPda', note: 'PDA(["mint_authority", wrapped_mint], FrSER…)' },
|
|
75
|
-
{ i: 4, writable: false, name: 'wrappedTokenProgram', note: 'must equal wrapped_mint.owner; Token-2022 on shares' },
|
|
76
|
-
{ i: 5, writable: true, name: 'depositorUnderlyingAta' },
|
|
77
|
-
{ i: 6, writable: false, signer: true, name: 'depositor', note: 'owner of account 5' },
|
|
78
|
-
{ i: 7, writable: false, name: 'unwrappedMint' },
|
|
79
|
-
{ i: 8, writable: false, name: 'unwrappedTokenProgram', note: 'must equal escrow.owner; not interchangeable with account 4' },
|
|
80
|
-
],
|
|
81
|
-
unwrap: {
|
|
82
|
-
accounts: 9,
|
|
83
|
-
account8: 'unwrapped token program',
|
|
84
|
-
prepend: 'idempotent ATA create for recipient underlying (may have been closed)',
|
|
85
|
-
note: '8 accounts → 0x70 TokenProgramMismatch',
|
|
86
|
-
},
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
/** Short 402-help / chat copy. Never dump raw Solana simulation logs. */
|
|
90
|
-
export function rewriteWrapClientError(message) {
|
|
91
|
-
const s = String(message ?? '');
|
|
92
|
-
if (/0x6a\b|custom program error:\s*106\b|NotEnoughAccounts/i.test(s)) {
|
|
93
|
-
return WRAP_TOO_FEW_ACCOUNTS;
|
|
94
|
-
}
|
|
95
|
-
if (/0x70\b|custom program error:\s*112\b|TokenProgramMismatch/i.test(s)) {
|
|
96
|
-
return 'unwrap ix is missing the unwrapped token program (account 8); 8-account unwrap is dead';
|
|
97
|
-
}
|
|
98
|
-
return s;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Machine-readable per-asset acquire directory published by the facilitator.
|
|
102
|
-
// Consulted first so newly listed twins work with zero code changes; on-chain
|
|
103
|
-
// derivation below is the fallback when the endpoint is unreachable.
|
|
104
|
-
const SUPPORTED_URL = process.env.OPENZOO_SUPPORTED_URL || 'https://x402.accrue.fund/supported';
|
|
105
|
-
let directoryCache = { at: 0, kinds: null };
|
|
106
|
-
|
|
107
|
-
async function acquireDirectory() {
|
|
108
|
-
if (directoryCache.kinds && Date.now() - directoryCache.at < 300000) return directoryCache.kinds;
|
|
109
|
-
try {
|
|
110
|
-
const r = await fetch(SUPPORTED_URL, { signal: AbortSignal.timeout(8000) });
|
|
111
|
-
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
112
|
-
const body = await r.json();
|
|
113
|
-
if (Array.isArray(body?.kinds)) directoryCache = { at: Date.now(), kinds: body.kinds };
|
|
114
|
-
} catch { /* endpoint down — fall back to chain derivation */ }
|
|
115
|
-
return directoryCache.kinds;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** /supported entry for a quoted Solana mint, or null. */
|
|
119
|
-
async function directoryEntryFor(wrappedMintStr) {
|
|
120
|
-
const kinds = await acquireDirectory();
|
|
121
|
-
if (!kinds) return null;
|
|
122
|
-
const row = kinds.find((k) => k?.network?.startsWith?.('solana:') && k?.extra?.asset === wrappedMintStr);
|
|
123
|
-
const acq = row?.extra?.acquire;
|
|
124
|
-
return acq?.method === 'spl-token-wrap' && acq.underlying?.address && acq.escrow ? acq : null;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function u64le(n) { const b = Buffer.alloc(8); b.writeBigUInt64LE(BigInt(n)); return b; }
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Resolve the pool behind a quoted mint. Returns null when the mint is not a
|
|
131
|
-
* wrap-nav twin (then there is nothing this module can do for it).
|
|
132
|
-
* Generic over whatever mint the 402 quotes — nothing hardcoded per twin.
|
|
133
|
-
*/
|
|
134
|
-
export async function resolvePool(connection, wrappedMintStr) {
|
|
135
|
-
const wrapped = new PublicKey(wrappedMintStr);
|
|
136
|
-
const info = await connection.getAccountInfo(wrapped);
|
|
137
|
-
if (!info) return null;
|
|
138
|
-
const wrappedProgram = info.owner;
|
|
139
|
-
let mint;
|
|
140
|
-
try { mint = unpackMint(wrapped, info, wrappedProgram); } catch { return null; }
|
|
141
|
-
|
|
142
|
-
// Preferred source: the facilitator's own /supported directory.
|
|
143
|
-
const acq = await directoryEntryFor(wrappedMintStr);
|
|
144
|
-
if (acq) {
|
|
145
|
-
const programId = new PublicKey(acq.program || WRAP_PROGRAM_ID);
|
|
146
|
-
const authority = new PublicKey(acq.mintAuthority);
|
|
147
|
-
let bump = acq.authorityBump;
|
|
148
|
-
if (bump == null) {
|
|
149
|
-
const [derived, derivedBump] = PublicKey.findProgramAddressSync(
|
|
150
|
-
[AUTHORITY_SEED, wrapped.toBuffer()], programId,
|
|
151
|
-
);
|
|
152
|
-
if (derived.equals(authority)) bump = derivedBump;
|
|
153
|
-
}
|
|
154
|
-
if (bump != null && mint.mintAuthority?.equals(authority)) {
|
|
155
|
-
return {
|
|
156
|
-
wrapped,
|
|
157
|
-
wrappedProgram,
|
|
158
|
-
programId,
|
|
159
|
-
authority,
|
|
160
|
-
bump,
|
|
161
|
-
escrow: new PublicKey(acq.escrow),
|
|
162
|
-
underlying: new PublicKey(acq.underlying.address),
|
|
163
|
-
underlyingProgram: new PublicKey(acq.underlying.tokenProgram || TOKEN_PROGRAM_ID),
|
|
164
|
-
underlyingDecimals: acq.underlying.decimals ?? 6,
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Fallback: derive everything from chain state.
|
|
170
|
-
const [authority, bump] = PublicKey.findProgramAddressSync(
|
|
171
|
-
[AUTHORITY_SEED, wrapped.toBuffer()], WRAP_PROGRAM_ID,
|
|
172
|
-
);
|
|
173
|
-
if (!mint.mintAuthority || !mint.mintAuthority.equals(authority)) return null;
|
|
174
|
-
|
|
175
|
-
// Prefer the on-chain backpointer registry; fall back to the authority's
|
|
176
|
-
// token accounts (older pools were created before registration existed).
|
|
177
|
-
let underlying; let escrow; let underlyingProgram;
|
|
178
|
-
const [backpointer] = PublicKey.findProgramAddressSync(
|
|
179
|
-
[BACKPOINTER_SEED, wrapped.toBuffer()], WRAP_PROGRAM_ID,
|
|
180
|
-
);
|
|
181
|
-
const bp = await connection.getAccountInfo(backpointer);
|
|
182
|
-
if (bp && bp.owner.equals(WRAP_PROGRAM_ID) && bp.data.length >= 96) {
|
|
183
|
-
underlying = new PublicKey(bp.data.subarray(0, 32));
|
|
184
|
-
escrow = new PublicKey(bp.data.subarray(32, 64));
|
|
185
|
-
underlyingProgram = new PublicKey(bp.data.subarray(64, 96));
|
|
186
|
-
} else {
|
|
187
|
-
for (const programId of [TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID]) {
|
|
188
|
-
const res = await connection.getTokenAccountsByOwner(authority, { programId });
|
|
189
|
-
if (res.value.length) {
|
|
190
|
-
const first = res.value[0];
|
|
191
|
-
const acc = unpackAccount(first.pubkey, first.account, programId);
|
|
192
|
-
escrow = first.pubkey;
|
|
193
|
-
underlying = acc.mint;
|
|
194
|
-
underlyingProgram = programId;
|
|
195
|
-
break;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
if (!escrow) return null;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const uInfo = await connection.getAccountInfo(underlying);
|
|
202
|
-
if (!uInfo) return null;
|
|
203
|
-
const uMint = unpackMint(underlying, uInfo, underlyingProgram);
|
|
204
|
-
return {
|
|
205
|
-
wrapped,
|
|
206
|
-
wrappedProgram,
|
|
207
|
-
programId: WRAP_PROGRAM_ID,
|
|
208
|
-
authority,
|
|
209
|
-
bump,
|
|
210
|
-
escrow,
|
|
211
|
-
underlying,
|
|
212
|
-
underlyingProgram,
|
|
213
|
-
underlyingDecimals: uMint.decimals,
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Underlying deposit required so that floor(deposit * supply / reserves)
|
|
219
|
-
* covers `sharesNeeded`. A small margin absorbs NAV drift between the read
|
|
220
|
-
* and the landing slot (donations/burns only ever reduce shares-per-asset).
|
|
221
|
-
*/
|
|
222
|
-
export function depositForShares(sharesNeeded, reserves, supply) {
|
|
223
|
-
if (supply === 0n || reserves === 0n) return sharesNeeded + MINIMUM_LIQUIDITY;
|
|
224
|
-
const exact = (sharesNeeded * reserves + supply - 1n) / supply; // ceil
|
|
225
|
-
return exact + exact / 200n + 2n; // +0.5% + 2 raw units of drift margin
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/** Current pool state: escrow reserves and wrapped supply, both raw bigint. */
|
|
229
|
-
export async function poolState(connection, pool) {
|
|
230
|
-
const [esc, sup] = await Promise.all([
|
|
231
|
-
connection.getTokenAccountBalance(pool.escrow).then((r) => BigInt(r.value.amount)).catch(() => 0n),
|
|
232
|
-
connection.getTokenSupply(pool.wrapped).then((r) => BigInt(r.value.amount)),
|
|
233
|
-
]);
|
|
234
|
-
return { reserves: esc, supply: sup };
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* ATA-create + 9-account Wrap. The program CPIs the deposit itself — there
|
|
239
|
-
* is no trailing TransferChecked. `rentPayer` funds ATA creation (defaults
|
|
240
|
-
* to the owner; the gateway feePayer when riding inside a payment tx).
|
|
241
|
-
*/
|
|
242
|
-
export function buildWrapInstructions({ pool, owner, depositRaw, rentPayer = owner }) {
|
|
243
|
-
const userWrapped = getAssociatedTokenAddressSync(pool.wrapped, owner, false, pool.wrappedProgram);
|
|
244
|
-
const userUnderlying = getAssociatedTokenAddressSync(pool.underlying, owner, false, pool.underlyingProgram);
|
|
245
|
-
// 9-account Wrap. Program CPIs the deposit. No TransferChecked after this.
|
|
246
|
-
// Old 5-account Wrap is rejected 0x6a (NotEnoughAccounts).
|
|
247
|
-
const wrapIx = new TransactionInstruction({
|
|
248
|
-
programId: pool.programId || WRAP_PROGRAM_ID,
|
|
249
|
-
keys: [
|
|
250
|
-
{ pubkey: pool.escrow, isSigner: false, isWritable: true },
|
|
251
|
-
{ pubkey: pool.wrapped, isSigner: false, isWritable: true },
|
|
252
|
-
{ pubkey: userWrapped, isSigner: false, isWritable: true },
|
|
253
|
-
{ pubkey: pool.authority, isSigner: false, isWritable: false },
|
|
254
|
-
{ pubkey: pool.wrappedProgram, isSigner: false, isWritable: false },
|
|
255
|
-
// the deposit the program will pull, and who authorises it
|
|
256
|
-
{ pubkey: userUnderlying, isSigner: false, isWritable: true },
|
|
257
|
-
{ pubkey: owner, isSigner: true, isWritable: false },
|
|
258
|
-
{ pubkey: pool.underlying, isSigner: false, isWritable: false },
|
|
259
|
-
{ pubkey: pool.underlyingProgram, isSigner: false, isWritable: false },
|
|
260
|
-
],
|
|
261
|
-
data: Buffer.concat([Buffer.from([1]), u64le(depositRaw), Buffer.from([pool.bump])]),
|
|
262
|
-
});
|
|
263
|
-
return [
|
|
264
|
-
createAssociatedTokenAccountIdempotentInstruction(rentPayer, userWrapped, owner, pool.wrapped, pool.wrappedProgram),
|
|
265
|
-
wrapIx,
|
|
266
|
-
];
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Confirm a signature by polling getSignatureStatuses over HTTP. Deliberately
|
|
271
|
-
* avoids connection.confirmTransaction: that path opens a websocket
|
|
272
|
-
* signatureSubscribe, and some RPC providers (fluxrpc) send a notification
|
|
273
|
-
* shape web3.js's schema rejects — the StructError fires inside the ws
|
|
274
|
-
* callback where no caller can catch it and takes down the whole process.
|
|
275
|
-
*/
|
|
276
|
-
export async function confirmSignatureByPolling(connection, signature, {
|
|
277
|
-
commitment = 'confirmed',
|
|
278
|
-
// 90s was too tight under load: OBSERVED "timed out after 90000ms waiting
|
|
279
|
-
// for confirmation" on a wrap that was still in flight. The cost of that
|
|
280
|
-
// timeout is not a slow reply — it is the asker having PAID and being told
|
|
281
|
-
// the call failed, because the transaction goes on confirming after we stop
|
|
282
|
-
// watching. Waiting longer is strictly cheaper than losing someone's money,
|
|
283
|
-
// so the default is generous and tunable.
|
|
284
|
-
timeoutMs = Number(process.env.OPENZOO_CONFIRM_TIMEOUT_MS || 900000),
|
|
285
|
-
pollMs = 1500,
|
|
286
|
-
} = {}) {
|
|
287
|
-
const accept = commitment === 'finalized' ? ['finalized'] : ['confirmed', 'finalized'];
|
|
288
|
-
const deadline = Date.now() + timeoutMs;
|
|
289
|
-
for (;;) {
|
|
290
|
-
let status = null;
|
|
291
|
-
try {
|
|
292
|
-
const res = await connection.getSignatureStatuses([signature]);
|
|
293
|
-
status = res?.value?.[0] ?? null;
|
|
294
|
-
} catch { /* transient RPC hiccup — keep polling until the deadline */ }
|
|
295
|
-
if (status) {
|
|
296
|
-
if (status.err) throw new Error(`transaction ${signature} failed on-chain: ${JSON.stringify(status.err)}`);
|
|
297
|
-
if (accept.includes(status.confirmationStatus)) return signature;
|
|
298
|
-
}
|
|
299
|
-
if (Date.now() >= deadline) {
|
|
300
|
-
// Say what actually happened. "Timed out" reads as "it did not go
|
|
301
|
-
// through", and the opposite is more likely: the signature is real and
|
|
302
|
-
// usually lands. Anyone reading this needs to check before re-sending,
|
|
303
|
-
// or they will pay twice.
|
|
304
|
-
throw new Error(
|
|
305
|
-
`timed out after ${timeoutMs}ms waiting for confirmation of ${signature} `
|
|
306
|
-
+ '— the transaction may STILL confirm; check it before retrying: '
|
|
307
|
-
+ `https://solscan.io/tx/${signature}`,
|
|
308
|
-
);
|
|
309
|
-
}
|
|
310
|
-
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/** Send a self-paid conversion tx and wait for confirmation. Returns the signature. */
|
|
315
|
-
export async function sendWrap(connection, keypair, pool, depositRaw) {
|
|
316
|
-
const ixs = buildWrapInstructions({ pool, owner: keypair.publicKey, depositRaw });
|
|
317
|
-
const tx = new Transaction().add(...ixs);
|
|
318
|
-
const { blockhash } = await connection.getLatestBlockhash('confirmed');
|
|
319
|
-
tx.recentBlockhash = blockhash;
|
|
320
|
-
tx.feePayer = keypair.publicKey;
|
|
321
|
-
tx.sign(keypair);
|
|
322
|
-
try {
|
|
323
|
-
const sig = await connection.sendRawTransaction(tx.serialize());
|
|
324
|
-
return confirmSignatureByPolling(connection, sig, { commitment: 'confirmed' });
|
|
325
|
-
} catch (err) {
|
|
326
|
-
const rewritten = rewriteWrapClientError(err?.message || String(err));
|
|
327
|
-
if (rewritten !== (err?.message || String(err))) {
|
|
328
|
-
throw new Error(rewritten);
|
|
329
|
-
}
|
|
330
|
-
throw err;
|
|
331
|
-
}
|
|
332
|
-
}
|