@getomnichain/omnichain 0.2.1 → 0.3.1
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/CHANGELOG.md +256 -0
- package/dist/cjs/chain.base.js.map +1 -1
- package/dist/cjs/errors.js +55 -6
- package/dist/cjs/errors.js.map +1 -1
- package/dist/cjs/evm/evm_chain.js +421 -2
- package/dist/cjs/evm/evm_chain.js.map +1 -1
- package/dist/cjs/evm/evm_transaction_status.js +4 -0
- package/dist/cjs/evm/evm_transaction_status.js.map +1 -1
- package/dist/cjs/evm/unsigned_evm_transaction.js +12 -0
- package/dist/cjs/evm/unsigned_evm_transaction.js.map +1 -1
- package/dist/cjs/solana/solana_chain.js +659 -21
- package/dist/cjs/solana/solana_chain.js.map +1 -1
- package/dist/cjs/solana/unsigned_solana_transaction.js +33 -0
- package/dist/cjs/solana/unsigned_solana_transaction.js.map +1 -1
- package/dist/cjs/utxo/utxo_chain.js +94 -10
- package/dist/cjs/utxo/utxo_chain.js.map +1 -1
- package/dist/esm/chain.base.d.ts +38 -1
- package/dist/esm/chain.base.d.ts.map +1 -1
- package/dist/esm/chain.base.js.map +1 -1
- package/dist/esm/errors.d.ts +13 -0
- package/dist/esm/errors.d.ts.map +1 -1
- package/dist/esm/errors.js +51 -6
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/evm/evm_chain.d.ts +48 -2
- package/dist/esm/evm/evm_chain.d.ts.map +1 -1
- package/dist/esm/evm/evm_chain.js +423 -4
- package/dist/esm/evm/evm_chain.js.map +1 -1
- package/dist/esm/evm/evm_transaction_status.d.ts +4 -0
- package/dist/esm/evm/evm_transaction_status.d.ts.map +1 -1
- package/dist/esm/evm/evm_transaction_status.js +4 -0
- package/dist/esm/evm/evm_transaction_status.js.map +1 -1
- package/dist/esm/evm/unsigned_evm_transaction.d.ts +13 -0
- package/dist/esm/evm/unsigned_evm_transaction.d.ts.map +1 -1
- package/dist/esm/evm/unsigned_evm_transaction.js +12 -0
- package/dist/esm/evm/unsigned_evm_transaction.js.map +1 -1
- package/dist/esm/solana/solana_chain.d.ts +100 -3
- package/dist/esm/solana/solana_chain.d.ts.map +1 -1
- package/dist/esm/solana/solana_chain.js +659 -23
- package/dist/esm/solana/solana_chain.js.map +1 -1
- package/dist/esm/solana/unsigned_solana_transaction.d.ts +20 -1
- package/dist/esm/solana/unsigned_solana_transaction.d.ts.map +1 -1
- package/dist/esm/solana/unsigned_solana_transaction.js +33 -0
- package/dist/esm/solana/unsigned_solana_transaction.js.map +1 -1
- package/dist/esm/utxo/utxo_chain.d.ts +15 -2
- package/dist/esm/utxo/utxo_chain.d.ts.map +1 -1
- package/dist/esm/utxo/utxo_chain.js +96 -13
- package/dist/esm/utxo/utxo_chain.js.map +1 -1
- package/docs/CONNECTIONS.md +33 -0
- package/docs/EIP7702.md +77 -0
- package/docs/JITO.md +41 -0
- package/docs/MULTI_RPC.md +28 -0
- package/package.json +1 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.SolanaChain = void 0;
|
|
4
|
+
exports.signatureBase58FromBytes = signatureBase58FromBytes;
|
|
5
|
+
exports.bs58encode = bs58encode;
|
|
4
6
|
const web3_js_1 = require("@solana/web3.js");
|
|
5
7
|
const spl_token_1 = require("@solana/spl-token");
|
|
6
8
|
const chain_base_ts_1 = require("../chain.base.js");
|
|
@@ -36,18 +38,26 @@ const SOL_PRIORITY_PERCENTILE = {
|
|
|
36
38
|
class SolanaChain extends chain_base_ts_1.Chain {
|
|
37
39
|
defaultRpcUrl;
|
|
38
40
|
rpcUrl;
|
|
41
|
+
rpcUrls;
|
|
39
42
|
legacyRpcEnvNames;
|
|
40
43
|
explorerClusterSuffix;
|
|
41
44
|
chainAgnosticGenesisHash;
|
|
45
|
+
jito;
|
|
42
46
|
_nativeToken;
|
|
43
47
|
_connection = null;
|
|
48
|
+
_rpcUrlIndex = 0;
|
|
44
49
|
constructor(init) {
|
|
45
50
|
super(init.chainId, init.name, network_type_ts_1.NetworkType.SOLANA, init.blockTimeSeconds, init.nativeSymbol, init.explorerBaseUrl);
|
|
46
51
|
this.defaultRpcUrl = init.defaultRpcUrl;
|
|
47
52
|
this.rpcUrl = init.rpcUrl;
|
|
53
|
+
this.rpcUrls = init.rpcUrls ?? [];
|
|
54
|
+
if (this.rpcUrls.length > 1) {
|
|
55
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.InvalidArgument, `SolanaChainInit.rpcUrls accepts only ONE endpoint in 0.3.0 (got ${this.rpcUrls.length}). Automatic failover retry is deferred to a follow-up release; passing >1 endpoint would silently discard entries.`, { chainId: init.chainId });
|
|
56
|
+
}
|
|
48
57
|
this.legacyRpcEnvNames = init.legacyRpcEnvNames ?? [];
|
|
49
58
|
this.explorerClusterSuffix = init.explorerClusterSuffix ?? '';
|
|
50
59
|
this.chainAgnosticGenesisHash = init.chainAgnosticGenesisHash;
|
|
60
|
+
this.jito = init.jito ?? null;
|
|
51
61
|
this._nativeToken = solana_token_ts_1.SolanaToken.native(init.chainId, init.nativeSymbol, init.nativeDecimals ?? 9);
|
|
52
62
|
(0, network_type_ts_1.registerNonEvmChain)(init.chainId, network_type_ts_1.NetworkType.SOLANA);
|
|
53
63
|
}
|
|
@@ -106,9 +116,35 @@ class SolanaChain extends chain_base_ts_1.Chain {
|
|
|
106
116
|
* the pre-rename `SOLANA_RPC_URL` here)
|
|
107
117
|
* 5. `defaultRpcUrl` (never throws — public cluster)
|
|
108
118
|
*/
|
|
119
|
+
resolvedRpcUrlForRedaction() {
|
|
120
|
+
try {
|
|
121
|
+
return this.readRpcUrl();
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async rpcWrap(fn, label) {
|
|
128
|
+
try {
|
|
129
|
+
return await fn();
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
if (err instanceof errors_ts_1.ChainError)
|
|
133
|
+
throw err;
|
|
134
|
+
const rpc = this.resolvedRpcUrlForRedaction();
|
|
135
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.RpcError, (0, errors_ts_1.sanitizeMessage)(`Solana ${label} failed on ${this.name}`, rpc), { chainId: this.chainId }, (0, errors_ts_1.sanitizeCause)(err, rpc));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
109
138
|
readRpcUrl() {
|
|
110
139
|
if (this.rpcUrl && this.rpcUrl.trim().length > 0)
|
|
111
140
|
return this.rpcUrl.trim();
|
|
141
|
+
if (this.rpcUrls.length > 0) {
|
|
142
|
+
for (const candidate of this.rpcUrls) {
|
|
143
|
+
if (candidate && candidate.trim().length > 0)
|
|
144
|
+
return candidate.trim();
|
|
145
|
+
}
|
|
146
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.RpcNotConfigured, `${this.name}: rpcUrls was supplied but every entry is blank; refusing to fall back to env or the public default cluster`, { chainId: this.chainId });
|
|
147
|
+
}
|
|
112
148
|
const env = globalThis.process?.env;
|
|
113
149
|
const candidates = [
|
|
114
150
|
this.name.replace(/ /g, '_').toUpperCase() + '_RPC_URL',
|
|
@@ -272,19 +308,39 @@ class SolanaChain extends chain_base_ts_1.Chain {
|
|
|
272
308
|
}
|
|
273
309
|
instructions.push((0, spl_token_1.createTransferCheckedInstruction)(sourceAta, mintPk, destAta, fromPk, amountMr, decimals, [], programId));
|
|
274
310
|
}
|
|
275
|
-
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash('finalized');
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
311
|
+
const { blockhash, lastValidBlockHeight } = await this.rpcWrap(() => connection.getLatestBlockhash('finalized'), 'getLatestBlockhash');
|
|
312
|
+
const alts = validateAltList(req.addressLookupTables, this.chainId);
|
|
313
|
+
let message;
|
|
314
|
+
try {
|
|
315
|
+
message = web3_js_1.MessageV0.compile({
|
|
316
|
+
payerKey: feePayerPk,
|
|
317
|
+
instructions,
|
|
318
|
+
recentBlockhash: blockhash,
|
|
319
|
+
addressLookupTableAccounts: alts,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
catch (err) {
|
|
323
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.InvalidArgument, `Solana createTransferUnsignedTransaction: MessageV0.compile failed — instruction or ALT layout invalid`, { chainId: this.chainId }, err instanceof Error ? err : undefined);
|
|
324
|
+
}
|
|
325
|
+
const tx = new web3_js_1.VersionedTransaction(message);
|
|
326
|
+
let serializedSize;
|
|
327
|
+
try {
|
|
328
|
+
serializedSize = tx.serialize().length;
|
|
329
|
+
}
|
|
330
|
+
catch (err) {
|
|
331
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.TransactionTooLarge, `Compiled Solana tx exceeds the serialize buffer; supply addressLookupTables to compress the account list`, { chainId: this.chainId }, err instanceof Error ? err : undefined);
|
|
332
|
+
}
|
|
333
|
+
if (serializedSize > 1232) {
|
|
334
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.TransactionTooLarge, `Compiled Solana transfer exceeds 1232-byte wire limit; supply addressLookupTables to compress the account list`, { chainId: this.chainId });
|
|
335
|
+
}
|
|
281
336
|
return new unsigned_solana_transaction_ts_1.UnsignedSolanaTransaction({
|
|
282
337
|
chainId: this.chainId,
|
|
283
|
-
transaction:
|
|
338
|
+
transaction: tx,
|
|
284
339
|
feePayer: feePayerAddress,
|
|
285
340
|
recentBlockhash: blockhash,
|
|
286
341
|
lastValidBlockHeight,
|
|
287
342
|
instructions,
|
|
343
|
+
addressLookupTables: alts,
|
|
288
344
|
});
|
|
289
345
|
}
|
|
290
346
|
/**
|
|
@@ -331,19 +387,39 @@ class SolanaChain extends chain_base_ts_1.Chain {
|
|
|
331
387
|
for (const ix of req.instructions)
|
|
332
388
|
allIxs.push(ix);
|
|
333
389
|
const connection = this.getConnection();
|
|
334
|
-
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash('finalized');
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
390
|
+
const { blockhash, lastValidBlockHeight } = await this.rpcWrap(() => connection.getLatestBlockhash('finalized'), 'getLatestBlockhash');
|
|
391
|
+
const alts = validateAltList(req.addressLookupTables, this.chainId);
|
|
392
|
+
let message;
|
|
393
|
+
try {
|
|
394
|
+
message = web3_js_1.MessageV0.compile({
|
|
395
|
+
payerKey: feePayerPk,
|
|
396
|
+
instructions: allIxs,
|
|
397
|
+
recentBlockhash: blockhash,
|
|
398
|
+
addressLookupTableAccounts: alts,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
catch (err) {
|
|
402
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.InvalidArgument, `Solana createInstructionsUnsignedTransaction: MessageV0.compile failed — instruction or ALT layout invalid`, { chainId: this.chainId }, err instanceof Error ? err : undefined);
|
|
403
|
+
}
|
|
404
|
+
const tx = new web3_js_1.VersionedTransaction(message);
|
|
405
|
+
let serializedSize;
|
|
406
|
+
try {
|
|
407
|
+
serializedSize = tx.serialize().length;
|
|
408
|
+
}
|
|
409
|
+
catch (err) {
|
|
410
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.TransactionTooLarge, `Compiled Solana tx exceeds the serialize buffer; supply addressLookupTables to compress the account list`, { chainId: this.chainId }, err instanceof Error ? err : undefined);
|
|
411
|
+
}
|
|
412
|
+
if (serializedSize > 1232) {
|
|
413
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.TransactionTooLarge, `Compiled Solana tx exceeds 1232-byte wire limit; supply addressLookupTables to compress the account list`, { chainId: this.chainId });
|
|
414
|
+
}
|
|
340
415
|
return new unsigned_solana_transaction_ts_1.UnsignedSolanaTransaction({
|
|
341
416
|
chainId: this.chainId,
|
|
342
|
-
transaction:
|
|
417
|
+
transaction: tx,
|
|
343
418
|
feePayer: feePayerAddress,
|
|
344
419
|
recentBlockhash: blockhash,
|
|
345
420
|
lastValidBlockHeight,
|
|
346
421
|
instructions: allIxs,
|
|
422
|
+
addressLookupTables: alts,
|
|
347
423
|
});
|
|
348
424
|
}
|
|
349
425
|
/**
|
|
@@ -355,13 +431,14 @@ class SolanaChain extends chain_base_ts_1.Chain {
|
|
|
355
431
|
*/
|
|
356
432
|
async refreshBlockhash(unsigned) {
|
|
357
433
|
const connection = this.getConnection();
|
|
358
|
-
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash('finalized');
|
|
434
|
+
const { blockhash, lastValidBlockHeight } = await this.rpcWrap(() => connection.getLatestBlockhash('finalized'), 'getLatestBlockhash');
|
|
359
435
|
if (blockhash === unsigned.recentBlockhash)
|
|
360
436
|
return unsigned;
|
|
361
437
|
const message = web3_js_1.MessageV0.compile({
|
|
362
438
|
payerKey: unsigned.feePayerPubkey,
|
|
363
439
|
instructions: unsigned.instructions,
|
|
364
440
|
recentBlockhash: blockhash,
|
|
441
|
+
addressLookupTableAccounts: unsigned.addressLookupTables,
|
|
365
442
|
});
|
|
366
443
|
return new unsigned_solana_transaction_ts_1.UnsignedSolanaTransaction({
|
|
367
444
|
chainId: unsigned.chainId,
|
|
@@ -370,6 +447,7 @@ class SolanaChain extends chain_base_ts_1.Chain {
|
|
|
370
447
|
recentBlockhash: blockhash,
|
|
371
448
|
lastValidBlockHeight,
|
|
372
449
|
instructions: unsigned.instructions,
|
|
450
|
+
addressLookupTables: unsigned.addressLookupTables,
|
|
373
451
|
});
|
|
374
452
|
}
|
|
375
453
|
/**
|
|
@@ -380,13 +458,13 @@ class SolanaChain extends chain_base_ts_1.Chain {
|
|
|
380
458
|
*/
|
|
381
459
|
async estimateAndApplyCu(unsigned) {
|
|
382
460
|
const connection = this.getConnection();
|
|
383
|
-
const sim = await connection.simulateTransaction(unsigned.transaction, {
|
|
461
|
+
const sim = await this.rpcWrap(() => connection.simulateTransaction(unsigned.transaction, {
|
|
384
462
|
sigVerify: false,
|
|
385
463
|
replaceRecentBlockhash: true,
|
|
386
464
|
commitment: 'confirmed',
|
|
387
|
-
});
|
|
465
|
+
}), 'simulateTransaction');
|
|
388
466
|
if (sim.value.err) {
|
|
389
|
-
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.
|
|
467
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.SimulationFailed, `Solana simulation rejected the tx: ${JSON.stringify(sim.value.err)}`, { chainId: this.chainId });
|
|
390
468
|
}
|
|
391
469
|
const consumed = sim.value.unitsConsumed;
|
|
392
470
|
if (!consumed)
|
|
@@ -406,6 +484,7 @@ class SolanaChain extends chain_base_ts_1.Chain {
|
|
|
406
484
|
payerKey: unsigned.feePayerPubkey,
|
|
407
485
|
instructions: newInstructions,
|
|
408
486
|
recentBlockhash: unsigned.recentBlockhash,
|
|
487
|
+
addressLookupTableAccounts: unsigned.addressLookupTables,
|
|
409
488
|
});
|
|
410
489
|
return new unsigned_solana_transaction_ts_1.UnsignedSolanaTransaction({
|
|
411
490
|
chainId: unsigned.chainId,
|
|
@@ -414,11 +493,12 @@ class SolanaChain extends chain_base_ts_1.Chain {
|
|
|
414
493
|
recentBlockhash: unsigned.recentBlockhash,
|
|
415
494
|
lastValidBlockHeight: unsigned.lastValidBlockHeight,
|
|
416
495
|
instructions: newInstructions,
|
|
496
|
+
addressLookupTables: unsigned.addressLookupTables,
|
|
417
497
|
});
|
|
418
498
|
}
|
|
419
499
|
/** Whether the given error came from a preflight simulation rejection (vs network/transport). */
|
|
420
500
|
static isSimulationError(err) {
|
|
421
|
-
if (err instanceof errors_ts_1.ChainError && err.kind === errors_ts_1.ChainErrorKinds.
|
|
501
|
+
if (err instanceof errors_ts_1.ChainError && err.kind === errors_ts_1.ChainErrorKinds.SimulationFailed)
|
|
422
502
|
return true;
|
|
423
503
|
const msg = err?.message ?? '';
|
|
424
504
|
return /simulation failed|transaction simulation failed/i.test(msg);
|
|
@@ -428,7 +508,68 @@ class SolanaChain extends chain_base_ts_1.Chain {
|
|
|
428
508
|
const msg = err?.message ?? '';
|
|
429
509
|
return /blockhash not found|expired blockhash|blockhash.*expired/i.test(msg);
|
|
430
510
|
}
|
|
431
|
-
async getTransactionStatus(txHash) {
|
|
511
|
+
async getTransactionStatus(txHash, opts) {
|
|
512
|
+
if (Array.isArray(txHash)) {
|
|
513
|
+
return runSolanaBatchStatus(txHash, (h, batchSignal) => {
|
|
514
|
+
const composedSignal = opts?.signal
|
|
515
|
+
? anySignal(opts.signal, batchSignal)
|
|
516
|
+
: batchSignal;
|
|
517
|
+
return this.getSingleSolanaStatus(h, { ...opts, signal: composedSignal });
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
return this.getSingleSolanaStatus(txHash, opts);
|
|
521
|
+
}
|
|
522
|
+
async getSingleSolanaStatus(txHash, opts) {
|
|
523
|
+
if (opts?.confirmations !== undefined && opts.confirmations > 1 && !opts.wait) {
|
|
524
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.InvalidArgument, `getTransactionStatus: confirmations > 1 requires wait: true (a single status read cannot enforce finality)`, { chainId: this.chainId, txHash });
|
|
525
|
+
}
|
|
526
|
+
if (!opts?.wait)
|
|
527
|
+
return this.getSolanaStatusOnce(txHash);
|
|
528
|
+
if (opts.signal?.aborted) {
|
|
529
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.InvalidArgument, `getTransactionStatus aborted before first poll`, { chainId: this.chainId, txHash });
|
|
530
|
+
}
|
|
531
|
+
const c = opts.confirmations ?? 1;
|
|
532
|
+
if (c !== 1 && c < 32) {
|
|
533
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.InvalidArgument, `Solana getTransactionStatus.confirmations must be 1 (confirmed) or >=32 (finalized); ${c} would ambiguously map otherwise.`, { chainId: this.chainId, txHash });
|
|
534
|
+
}
|
|
535
|
+
if (opts.timeoutMs === undefined) {
|
|
536
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.InvalidArgument, `getTransactionStatus: wait: true requires an explicit timeoutMs. Unbounded polling would pin an RPC worker + connection forever if the tx is dropped.`, { chainId: this.chainId, txHash });
|
|
537
|
+
}
|
|
538
|
+
if (opts.timeoutMs < 0) {
|
|
539
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.InvalidArgument, `getTransactionStatus: timeoutMs must be >= 0`, { chainId: this.chainId, txHash });
|
|
540
|
+
}
|
|
541
|
+
const deadline = Date.now() + opts.timeoutMs;
|
|
542
|
+
const pollMs = Math.max(400, this.blockTimeSeconds * 1000);
|
|
543
|
+
const wantFinalized = c >= 32;
|
|
544
|
+
let last;
|
|
545
|
+
while (true) {
|
|
546
|
+
last = await this.getSolanaStatusOnce(txHash);
|
|
547
|
+
if (last.status === 'Success' || last.status === 'Failed') {
|
|
548
|
+
if (!wantFinalized)
|
|
549
|
+
return last;
|
|
550
|
+
let sigStatus;
|
|
551
|
+
try {
|
|
552
|
+
sigStatus = await this.getConnection().getSignatureStatus(txHash, { searchTransactionHistory: true });
|
|
553
|
+
}
|
|
554
|
+
catch (err) {
|
|
555
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.RpcError, (0, errors_ts_1.sanitizeMessage)(`Failed to poll signature status for finalization on ${this.name}`, this.resolvedRpcUrlForRedaction()), { chainId: this.chainId, txHash }, (0, errors_ts_1.sanitizeCause)(err, this.resolvedRpcUrlForRedaction()));
|
|
556
|
+
}
|
|
557
|
+
if (sigStatus.value?.confirmationStatus === 'finalized')
|
|
558
|
+
return last;
|
|
559
|
+
if (Date.now() >= deadline) {
|
|
560
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.RpcError, `getTransactionStatus timed out after ${opts.timeoutMs}ms before reaching finalized; last confirmation status: ${sigStatus.value?.confirmationStatus ?? 'unknown'}. Consumer must NOT credit as final.`, { chainId: this.chainId, txHash });
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
else if (Date.now() >= deadline) {
|
|
564
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.RpcError, `getTransactionStatus timed out after ${opts.timeoutMs}ms; last observed status was ${last.status}. Consumer must NOT credit as final AND must NOT re-sign — the tx may still land. Re-broadcast the same signed bytes or continue polling with the same txHash.`, { chainId: this.chainId, txHash });
|
|
565
|
+
}
|
|
566
|
+
await solanaInterruptibleSleep(pollMs, opts.signal);
|
|
567
|
+
if (opts.signal?.aborted) {
|
|
568
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.InvalidArgument, `getTransactionStatus aborted mid-poll`, { chainId: this.chainId, txHash });
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
async getSolanaStatusOnce(txHash) {
|
|
432
573
|
const connection = this.getConnection();
|
|
433
574
|
// Best-effort read of the current rpc URL for message/cause scrubbing.
|
|
434
575
|
// readRpcUrl throws when nothing is configured; there'd be no
|
|
@@ -536,7 +677,383 @@ class SolanaChain extends chain_base_ts_1.Chain {
|
|
|
536
677
|
});
|
|
537
678
|
}
|
|
538
679
|
async getChainTipHeight() {
|
|
539
|
-
return this.getConnection().getSlot('confirmed');
|
|
680
|
+
return this.rpcWrap(() => this.getConnection().getSlot('confirmed'), 'getSlot');
|
|
681
|
+
}
|
|
682
|
+
async getLatestBlockhash(commitment = 'confirmed') {
|
|
683
|
+
return this.rpcWrap(async () => {
|
|
684
|
+
const { blockhash, lastValidBlockHeight } = await this.getConnection().getLatestBlockhash(commitment);
|
|
685
|
+
return { blockhash, lastValidBlockHeight };
|
|
686
|
+
}, `getLatestBlockhash(${commitment})`);
|
|
687
|
+
}
|
|
688
|
+
async simulateTransaction(signed, opts) {
|
|
689
|
+
let bytes;
|
|
690
|
+
if (typeof signed === 'string') {
|
|
691
|
+
const stripped = signed.startsWith('0x') ? signed.slice(2) : signed;
|
|
692
|
+
if (!/^[0-9a-fA-F]+$/.test(stripped) || stripped.length % 2 !== 0 || stripped.length === 0) {
|
|
693
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.InvalidArgument, `Solana simulateTransaction: signed must be Uint8Array or 0x-prefixed hex; got malformed string (len ${signed.length}).`, { chainId: this.chainId });
|
|
694
|
+
}
|
|
695
|
+
bytes = new Uint8Array(Buffer.from(stripped, 'hex'));
|
|
696
|
+
}
|
|
697
|
+
else {
|
|
698
|
+
bytes = signed;
|
|
699
|
+
}
|
|
700
|
+
if (bytes.length < 65) {
|
|
701
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.InvalidArgument, `Solana simulateTransaction: signed bytes must be >= 65 bytes (1 sig-count + 64-byte signature), got ${bytes.length}`, { chainId: this.chainId });
|
|
702
|
+
}
|
|
703
|
+
let tx;
|
|
704
|
+
try {
|
|
705
|
+
tx = web3_js_1.VersionedTransaction.deserialize(bytes);
|
|
706
|
+
}
|
|
707
|
+
catch (err) {
|
|
708
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.InvalidArgument, `Solana simulateTransaction: could not deserialize signed bytes into a VersionedTransaction`, { chainId: this.chainId }, err instanceof Error ? err : undefined);
|
|
709
|
+
}
|
|
710
|
+
return this.rpcWrap(async () => {
|
|
711
|
+
const sim = await this.getConnection().simulateTransaction(tx, {
|
|
712
|
+
sigVerify: opts?.sigVerify ?? false,
|
|
713
|
+
replaceRecentBlockhash: opts?.replaceRecentBlockhash ?? false,
|
|
714
|
+
commitment: opts?.commitment,
|
|
715
|
+
});
|
|
716
|
+
return {
|
|
717
|
+
unitsConsumed: sim.value.unitsConsumed ?? null,
|
|
718
|
+
err: sim.value.err ?? null,
|
|
719
|
+
logs: sim.value.logs ?? null,
|
|
720
|
+
};
|
|
721
|
+
}, 'simulateTransaction');
|
|
722
|
+
}
|
|
723
|
+
async broadcast(signed, opts) {
|
|
724
|
+
if (opts && opts.signal !== undefined) {
|
|
725
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.FeatureNotSupported, `Solana broadcast: signal is not honored in 0.3.0 (silently ignoring would let a caller conclude 'not sent' while the tx still lands). Cancellation returns in 0.3.1.`, { chainId: this.chainId });
|
|
726
|
+
}
|
|
727
|
+
let bytes;
|
|
728
|
+
if (typeof signed === 'string') {
|
|
729
|
+
const stripped = signed.startsWith('0x') ? signed.slice(2) : signed;
|
|
730
|
+
if (!/^[0-9a-fA-F]+$/.test(stripped) || stripped.length % 2 !== 0 || stripped.length === 0) {
|
|
731
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.InvalidArgument, `Solana broadcast: signed must be Uint8Array or 0x-prefixed hex; got malformed string (len ${signed.length}). Base58/base64 encodings are NOT accepted — decode to bytes first.`, { chainId: this.chainId });
|
|
732
|
+
}
|
|
733
|
+
bytes = new Uint8Array(Buffer.from(stripped, 'hex'));
|
|
734
|
+
}
|
|
735
|
+
else {
|
|
736
|
+
bytes = signed;
|
|
737
|
+
}
|
|
738
|
+
if (bytes.length < 65) {
|
|
739
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.InvalidArgument, `Solana broadcast: signed bytes must be >= 65 bytes (1 sig-count + 64-byte signature), got ${bytes.length}`, { chainId: this.chainId });
|
|
740
|
+
}
|
|
741
|
+
if (bytes.length > 1232) {
|
|
742
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.TransactionTooLarge, `Solana broadcast: signed tx is ${bytes.length} bytes, exceeds 1232-byte wire limit`, { chainId: this.chainId });
|
|
743
|
+
}
|
|
744
|
+
if (opts?.via === 'jito') {
|
|
745
|
+
if (!this.jito) {
|
|
746
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.FeatureNotSupported, `Solana broadcast: via: 'jito' requires SolanaChain to be constructed with jito config`, { chainId: this.chainId });
|
|
747
|
+
}
|
|
748
|
+
if (opts.skipPreflight !== undefined || opts.maxRetries !== undefined) {
|
|
749
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.InvalidArgument, `Solana broadcast: skipPreflight/maxRetries do not apply to the Jito path (block-engine has its own retry semantics)`, { chainId: this.chainId });
|
|
750
|
+
}
|
|
751
|
+
const signature = signatureBase58FromBytes(bytes);
|
|
752
|
+
await this.submitJitoBundle([bytes]);
|
|
753
|
+
return signature;
|
|
754
|
+
}
|
|
755
|
+
try {
|
|
756
|
+
const sig = await this.getConnection().sendRawTransaction(bytes, {
|
|
757
|
+
skipPreflight: opts?.skipPreflight ?? false,
|
|
758
|
+
maxRetries: opts?.maxRetries,
|
|
759
|
+
});
|
|
760
|
+
return sig;
|
|
761
|
+
}
|
|
762
|
+
catch (err) {
|
|
763
|
+
const rawMsg = (err instanceof Error ? err.message : String(err)).toLowerCase();
|
|
764
|
+
if (rawMsg.includes('already been processed') || rawMsg.includes('already processed')) {
|
|
765
|
+
return signatureBase58FromBytes(bytes);
|
|
766
|
+
}
|
|
767
|
+
throw this.classifyBroadcastError(err);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
async submitJitoBundle(signedTxs, opts) {
|
|
771
|
+
if (!this.jito) {
|
|
772
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.FeatureNotSupported, 'Jito bundle submission requires SolanaChain to be constructed with jito config', { chainId: this.chainId });
|
|
773
|
+
}
|
|
774
|
+
const body = {
|
|
775
|
+
jsonrpc: '2.0',
|
|
776
|
+
id: 1,
|
|
777
|
+
method: 'sendBundle',
|
|
778
|
+
params: [
|
|
779
|
+
signedTxs.map((b) => Buffer.from(b).toString('base64')),
|
|
780
|
+
{ encoding: 'base64' },
|
|
781
|
+
],
|
|
782
|
+
};
|
|
783
|
+
const jitoUrl = this.jito.url;
|
|
784
|
+
const signal = combineJitoSignal(opts?.signal, opts?.timeoutMs ?? 15_000);
|
|
785
|
+
let json;
|
|
786
|
+
try {
|
|
787
|
+
const resp = await fetch(jitoUrl, {
|
|
788
|
+
method: 'POST',
|
|
789
|
+
headers: {
|
|
790
|
+
'Content-Type': 'application/json',
|
|
791
|
+
...(this.jito.auth ? { Authorization: `Bearer ${this.jito.auth}` } : {}),
|
|
792
|
+
},
|
|
793
|
+
body: JSON.stringify(body),
|
|
794
|
+
signal,
|
|
795
|
+
});
|
|
796
|
+
if (!resp.ok) {
|
|
797
|
+
// 401/403 = jito auth config problem, not a bundle rejection.
|
|
798
|
+
// Treat as RpcError so consumer doesn't push the tx toward re-sign.
|
|
799
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.RpcError, (0, errors_ts_1.sanitizeMessage)(`Jito submitBundle HTTP ${resp.status} on ${this.name}`, jitoUrl), { chainId: this.chainId });
|
|
800
|
+
}
|
|
801
|
+
json = (await resp.json());
|
|
802
|
+
}
|
|
803
|
+
catch (err) {
|
|
804
|
+
if (err instanceof errors_ts_1.ChainError)
|
|
805
|
+
throw err;
|
|
806
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.RpcError, (0, errors_ts_1.sanitizeMessage)(`Jito submitBundle transport failed on ${this.name}`, jitoUrl), { chainId: this.chainId }, (0, errors_ts_1.sanitizeCause)(err, jitoUrl));
|
|
807
|
+
}
|
|
808
|
+
if (json.error || !json.result) {
|
|
809
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.BroadcastRejected, (0, errors_ts_1.sanitizeMessage)(`Jito submitBundle rejected on ${this.name}: ${json.error?.message ?? 'no bundle id returned'}`, jitoUrl), { chainId: this.chainId });
|
|
810
|
+
}
|
|
811
|
+
return json.result;
|
|
812
|
+
}
|
|
813
|
+
async getBundleStatus(bundleId, opts) {
|
|
814
|
+
if (!this.jito) {
|
|
815
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.FeatureNotSupported, 'Jito getBundleStatus requires SolanaChain to be constructed with jito config', { chainId: this.chainId });
|
|
816
|
+
}
|
|
817
|
+
const ids = Array.isArray(bundleId) ? bundleId : [bundleId];
|
|
818
|
+
const body = {
|
|
819
|
+
jsonrpc: '2.0',
|
|
820
|
+
id: 1,
|
|
821
|
+
method: 'getBundleStatuses',
|
|
822
|
+
params: [ids],
|
|
823
|
+
};
|
|
824
|
+
let json;
|
|
825
|
+
const jitoUrl = this.jito.url;
|
|
826
|
+
const signal = combineJitoSignal(opts?.signal, opts?.timeoutMs ?? 15_000);
|
|
827
|
+
try {
|
|
828
|
+
const resp = await fetch(jitoUrl, {
|
|
829
|
+
method: 'POST',
|
|
830
|
+
headers: {
|
|
831
|
+
'Content-Type': 'application/json',
|
|
832
|
+
...(this.jito.auth ? { Authorization: `Bearer ${this.jito.auth}` } : {}),
|
|
833
|
+
},
|
|
834
|
+
body: JSON.stringify(body),
|
|
835
|
+
signal,
|
|
836
|
+
});
|
|
837
|
+
if (!resp.ok) {
|
|
838
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.RpcError, (0, errors_ts_1.sanitizeMessage)(`Jito getBundleStatuses HTTP ${resp.status} on ${this.name}`, jitoUrl), { chainId: this.chainId });
|
|
839
|
+
}
|
|
840
|
+
json = (await resp.json());
|
|
841
|
+
}
|
|
842
|
+
catch (err) {
|
|
843
|
+
if (err instanceof errors_ts_1.ChainError)
|
|
844
|
+
throw err;
|
|
845
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.RpcError, (0, errors_ts_1.sanitizeMessage)(`Jito getBundleStatuses transport failed on ${this.name}`, jitoUrl), { chainId: this.chainId }, (0, errors_ts_1.sanitizeCause)(err, jitoUrl));
|
|
846
|
+
}
|
|
847
|
+
if (json.error) {
|
|
848
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.RpcError, (0, errors_ts_1.sanitizeMessage)(`Jito getBundleStatuses error on ${this.name}: ${json.error.message ?? 'unknown'}`, jitoUrl), { chainId: this.chainId });
|
|
849
|
+
}
|
|
850
|
+
const rawValue = json.result?.value;
|
|
851
|
+
if (rawValue !== undefined && rawValue !== null && !Array.isArray(rawValue)) {
|
|
852
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.RpcError, (0, errors_ts_1.sanitizeMessage)(`Jito getBundleStatuses returned malformed value (expected array or null)`, jitoUrl), { chainId: this.chainId });
|
|
853
|
+
}
|
|
854
|
+
const rows = Array.isArray(rawValue) ? rawValue : [];
|
|
855
|
+
const statuses = ids.map((id) => {
|
|
856
|
+
const row = rows.find((r) => r != null && r.bundle_id === id);
|
|
857
|
+
if (!row)
|
|
858
|
+
return { bundleId: id, state: 'Pending' };
|
|
859
|
+
// Jito's err field varies: null (success), { Ok: null } (success),
|
|
860
|
+
// { Err: <anything> } (failed), or a plain string (some proxies).
|
|
861
|
+
// Anything that's non-null AND not { Ok: null } means failure.
|
|
862
|
+
const rawErr = row.err;
|
|
863
|
+
const isOk = rawErr === null || rawErr === undefined
|
|
864
|
+
|| (typeof rawErr === 'object' && 'Ok' in rawErr && rawErr.Ok === null && !('Err' in rawErr));
|
|
865
|
+
const errStr = isOk
|
|
866
|
+
? undefined
|
|
867
|
+
: typeof rawErr === 'string'
|
|
868
|
+
? rawErr
|
|
869
|
+
: typeof rawErr === 'object' && rawErr !== null && 'Err' in rawErr
|
|
870
|
+
? String(rawErr.Err)
|
|
871
|
+
: JSON.stringify(rawErr);
|
|
872
|
+
const confStatus = row.confirmation_status;
|
|
873
|
+
const state = errStr !== undefined
|
|
874
|
+
? 'Failed'
|
|
875
|
+
: (confStatus === 'finalized' || confStatus === 'confirmed' ? 'Landed' : 'Pending');
|
|
876
|
+
return { bundleId: id, state, slot: row.slot, err: errStr };
|
|
877
|
+
});
|
|
878
|
+
return Array.isArray(bundleId) ? statuses : statuses[0];
|
|
879
|
+
}
|
|
880
|
+
async createUnsignedTransaction(req) {
|
|
881
|
+
if (req && req.signal !== undefined) {
|
|
882
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.FeatureNotSupported, `Solana createUnsignedTransaction: signal is not honored in 0.3.0 (silently ignoring would let a caller conclude 'not built' while getLatestBlockhash continues). Cancellation returns in 0.3.1.`, { chainId: this.chainId });
|
|
883
|
+
}
|
|
884
|
+
if (!req.payer) {
|
|
885
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.InvalidArgument, 'CreateSolanaUnsignedTransactionRequest.payer is required', { chainId: this.chainId });
|
|
886
|
+
}
|
|
887
|
+
let payerKey;
|
|
888
|
+
try {
|
|
889
|
+
payerKey = new web3_js_1.PublicKey(req.payer);
|
|
890
|
+
}
|
|
891
|
+
catch (err) {
|
|
892
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.InvalidAddress, `Invalid Solana payer pubkey: ${req.payer}`, { chainId: this.chainId, address: req.payer }, err instanceof Error ? err : undefined);
|
|
893
|
+
}
|
|
894
|
+
if (!Array.isArray(req.instructions) || req.instructions.length === 0) {
|
|
895
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.InvalidArgument, 'CreateSolanaUnsignedTransactionRequest.instructions must be a non-empty array', { chainId: this.chainId });
|
|
896
|
+
}
|
|
897
|
+
const connection = this.getConnection();
|
|
898
|
+
let blockhash;
|
|
899
|
+
let lastValidBlockHeight;
|
|
900
|
+
try {
|
|
901
|
+
const bh = await connection.getLatestBlockhash('finalized');
|
|
902
|
+
blockhash = bh.blockhash;
|
|
903
|
+
lastValidBlockHeight = bh.lastValidBlockHeight;
|
|
904
|
+
}
|
|
905
|
+
catch (err) {
|
|
906
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.RpcError, (0, errors_ts_1.sanitizeMessage)(`Solana createUnsignedTransaction: getLatestBlockhash failed on ${this.name}`, this.resolvedRpcUrlForRedaction()), { chainId: this.chainId }, (0, errors_ts_1.sanitizeCause)(err, this.resolvedRpcUrlForRedaction()));
|
|
907
|
+
}
|
|
908
|
+
let message;
|
|
909
|
+
try {
|
|
910
|
+
message = web3_js_1.MessageV0.compile({
|
|
911
|
+
payerKey,
|
|
912
|
+
instructions: req.instructions,
|
|
913
|
+
recentBlockhash: blockhash,
|
|
914
|
+
addressLookupTableAccounts: req.addressLookupTables ?? [],
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
catch (err) {
|
|
918
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.InvalidArgument, `Solana createUnsignedTransaction: MessageV0.compile failed — instruction or ALT layout invalid`, { chainId: this.chainId }, err instanceof Error ? err : undefined);
|
|
919
|
+
}
|
|
920
|
+
const tx = new web3_js_1.VersionedTransaction(message);
|
|
921
|
+
let serializedSize;
|
|
922
|
+
try {
|
|
923
|
+
serializedSize = tx.serialize().length;
|
|
924
|
+
}
|
|
925
|
+
catch (err) {
|
|
926
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.TransactionTooLarge, `Compiled Solana tx exceeds the serialize buffer; supply addressLookupTables to compress the account list`, { chainId: this.chainId }, err instanceof Error ? err : undefined);
|
|
927
|
+
}
|
|
928
|
+
if (serializedSize > 1232) {
|
|
929
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.TransactionTooLarge, `Compiled Solana tx exceeds 1232-byte wire limit; supply addressLookupTables to reduce size`, { chainId: this.chainId });
|
|
930
|
+
}
|
|
931
|
+
return new unsigned_solana_transaction_ts_1.UnsignedSolanaTransaction({
|
|
932
|
+
chainId: this.chainId,
|
|
933
|
+
transaction: tx,
|
|
934
|
+
feePayer: req.payer,
|
|
935
|
+
recentBlockhash: blockhash,
|
|
936
|
+
lastValidBlockHeight,
|
|
937
|
+
instructions: req.instructions,
|
|
938
|
+
addressLookupTables: req.addressLookupTables,
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
async getAccountInfo(pubkey) {
|
|
942
|
+
const connection = this.getConnection();
|
|
943
|
+
const toPk = (raw) => {
|
|
944
|
+
try {
|
|
945
|
+
return new web3_js_1.PublicKey(raw);
|
|
946
|
+
}
|
|
947
|
+
catch (err) {
|
|
948
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.InvalidAddress, `Invalid Solana pubkey: ${raw}`, { chainId: this.chainId, address: raw }, err instanceof Error ? err : undefined);
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
|
+
if (Array.isArray(pubkey)) {
|
|
952
|
+
const pks = pubkey.map(toPk);
|
|
953
|
+
try {
|
|
954
|
+
const infos = await connection.getMultipleAccountsInfo(pks);
|
|
955
|
+
return infos.map((info) => (info === null ? null : toAccountInfoResult(info)));
|
|
956
|
+
}
|
|
957
|
+
catch (err) {
|
|
958
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.RpcError, (0, errors_ts_1.sanitizeMessage)(`Solana getMultipleAccountsInfo RPC failure on ${this.name}`, this.resolvedRpcUrlForRedaction()), { chainId: this.chainId }, (0, errors_ts_1.sanitizeCause)(err, this.resolvedRpcUrlForRedaction()));
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
try {
|
|
962
|
+
const info = await connection.getAccountInfo(toPk(pubkey));
|
|
963
|
+
return info === null ? null : toAccountInfoResult(info);
|
|
964
|
+
}
|
|
965
|
+
catch (err) {
|
|
966
|
+
if (err instanceof errors_ts_1.ChainError)
|
|
967
|
+
throw err;
|
|
968
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.RpcError, (0, errors_ts_1.sanitizeMessage)(`Solana getAccountInfo RPC failure on ${this.name}`, this.resolvedRpcUrlForRedaction()), { chainId: this.chainId, address: pubkey }, (0, errors_ts_1.sanitizeCause)(err, this.resolvedRpcUrlForRedaction()));
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
async getTokenAccount(owner, mint, opts) {
|
|
972
|
+
let ownerPk;
|
|
973
|
+
let mintPk;
|
|
974
|
+
try {
|
|
975
|
+
ownerPk = new web3_js_1.PublicKey(owner);
|
|
976
|
+
}
|
|
977
|
+
catch (err) {
|
|
978
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.InvalidAddress, `Invalid Solana owner pubkey: ${owner}`, { chainId: this.chainId, address: owner }, err instanceof Error ? err : undefined);
|
|
979
|
+
}
|
|
980
|
+
try {
|
|
981
|
+
mintPk = new web3_js_1.PublicKey(mint);
|
|
982
|
+
}
|
|
983
|
+
catch (err) {
|
|
984
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.InvalidTokenIdentifier, `Invalid Solana mint pubkey: ${mint}`, { chainId: this.chainId, identifier: mint }, err instanceof Error ? err : undefined);
|
|
985
|
+
}
|
|
986
|
+
let programId;
|
|
987
|
+
try {
|
|
988
|
+
programId = await this.resolveTokenProgramId(mintPk);
|
|
989
|
+
}
|
|
990
|
+
catch (err) {
|
|
991
|
+
if (err instanceof errors_ts_1.ChainError)
|
|
992
|
+
throw err;
|
|
993
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.RpcError, (0, errors_ts_1.sanitizeMessage)(`Solana getTokenAccount: mint-program lookup failed on ${this.name}`, this.resolvedRpcUrlForRedaction()), { chainId: this.chainId, identifier: mint }, (0, errors_ts_1.sanitizeCause)(err, this.resolvedRpcUrlForRedaction()));
|
|
994
|
+
}
|
|
995
|
+
let ataPk;
|
|
996
|
+
try {
|
|
997
|
+
ataPk = (0, spl_token_1.getAssociatedTokenAddressSync)(mintPk, ownerPk, opts?.allowOwnerOffCurve ?? false, programId);
|
|
998
|
+
}
|
|
999
|
+
catch (err) {
|
|
1000
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.InvalidArgument, `ATA derivation failed: ${err instanceof Error ? err.message : String(err)}`, { chainId: this.chainId, address: owner }, err instanceof Error ? err : undefined);
|
|
1001
|
+
}
|
|
1002
|
+
try {
|
|
1003
|
+
const acc = await (0, spl_token_1.getAccount)(this.getConnection(), ataPk, undefined, programId);
|
|
1004
|
+
return { ata: ataPk.toBase58(), exists: true, balanceMr: acc.amount };
|
|
1005
|
+
}
|
|
1006
|
+
catch (err) {
|
|
1007
|
+
if (err instanceof spl_token_1.TokenAccountNotFoundError) {
|
|
1008
|
+
return { ata: ataPk.toBase58(), exists: false, balanceMr: undefined };
|
|
1009
|
+
}
|
|
1010
|
+
if (err instanceof spl_token_1.TokenInvalidAccountOwnerError) {
|
|
1011
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.InvalidArgument, `Solana getTokenAccount: account at ${ataPk.toBase58()} exists but is owned by an unexpected program (hostile squat or wrong mint program). Consumer should NOT create-ATA blindly.`, { chainId: this.chainId, address: ataPk.toBase58() }, err);
|
|
1012
|
+
}
|
|
1013
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.RpcError, (0, errors_ts_1.sanitizeMessage)(`Solana getTokenAccount RPC failure on ${this.name}`, this.resolvedRpcUrlForRedaction()), { chainId: this.chainId, address: ataPk.toBase58() }, (0, errors_ts_1.sanitizeCause)(err, this.resolvedRpcUrlForRedaction()));
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
async fetchAddressLookupTable(altAddress) {
|
|
1017
|
+
let pk;
|
|
1018
|
+
try {
|
|
1019
|
+
pk = new web3_js_1.PublicKey(altAddress);
|
|
1020
|
+
}
|
|
1021
|
+
catch (err) {
|
|
1022
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.InvalidAddress, `Invalid ALT pubkey: ${altAddress}`, { chainId: this.chainId, address: altAddress }, err instanceof Error ? err : undefined);
|
|
1023
|
+
}
|
|
1024
|
+
let res;
|
|
1025
|
+
try {
|
|
1026
|
+
res = await this.getConnection().getAddressLookupTable(pk);
|
|
1027
|
+
}
|
|
1028
|
+
catch (err) {
|
|
1029
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.RpcError, (0, errors_ts_1.sanitizeMessage)(`Solana fetchAddressLookupTable RPC failure on ${this.name}`, this.resolvedRpcUrlForRedaction()), { chainId: this.chainId, address: altAddress }, (0, errors_ts_1.sanitizeCause)(err, this.resolvedRpcUrlForRedaction()));
|
|
1030
|
+
}
|
|
1031
|
+
if (!res.value) {
|
|
1032
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.InvalidArgument, `Address lookup table ${altAddress} not found`, { chainId: this.chainId, address: altAddress });
|
|
1033
|
+
}
|
|
1034
|
+
return res.value;
|
|
1035
|
+
}
|
|
1036
|
+
classifyBroadcastError(err) {
|
|
1037
|
+
const rpc = this.resolvedRpcUrlForRedaction();
|
|
1038
|
+
const msg = err instanceof Error ? err.message.toLowerCase() : String(err).toLowerCase();
|
|
1039
|
+
const safeCause = (0, errors_ts_1.sanitizeCause)(err, rpc);
|
|
1040
|
+
const transportSignals = /econnreset|econnrefused|econnaborted|etimedout|enotfound|network request failed|fetch failed|socket hang up|too\s+many\s+requests|rate.?limit/;
|
|
1041
|
+
if (/blockhash\s+not\s+found|blockhash\s+expired|block\s+height\s+exceeded/.test(msg)) {
|
|
1042
|
+
return new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.BlockhashExpired, (0, errors_ts_1.sanitizeMessage)(`Solana broadcast rejected: blockhash expired on ${this.name}`, rpc), { chainId: this.chainId }, safeCause);
|
|
1043
|
+
}
|
|
1044
|
+
// Preflight/simulation MUST be checked before the size predicate — Solana
|
|
1045
|
+
// preflight errors routinely embed "exceeds <N>" for CU / account-count
|
|
1046
|
+
// limits, and would otherwise be misclassified as TransactionTooLarge.
|
|
1047
|
+
if (msg.includes('preflight') || msg.includes('simulation failed') || msg.includes('sendtransactionerror')) {
|
|
1048
|
+
return new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.SimulationFailed, (0, errors_ts_1.sanitizeMessage)(`Solana broadcast rejected: preflight simulation failed on ${this.name}`, rpc), { chainId: this.chainId }, safeCause);
|
|
1049
|
+
}
|
|
1050
|
+
if (/transaction\s+too\s+large|encoded\/raw transaction size exceeds|exceeds\s+the\s+maximum\s+transaction\s+size/.test(msg)) {
|
|
1051
|
+
return new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.TransactionTooLarge, (0, errors_ts_1.sanitizeMessage)(`Solana broadcast rejected: transaction exceeds 1232 bytes on ${this.name} (use ALT)`, rpc), { chainId: this.chainId }, safeCause);
|
|
1052
|
+
}
|
|
1053
|
+
if (transportSignals.test(msg)) {
|
|
1054
|
+
return new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.RpcError, (0, errors_ts_1.sanitizeMessage)(`Solana broadcast RPC transport failure on ${this.name}`, rpc), { chainId: this.chainId }, safeCause);
|
|
1055
|
+
}
|
|
1056
|
+
return new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.BroadcastRejected, (0, errors_ts_1.sanitizeMessage)(`Solana broadcast rejected on ${this.name}`, rpc), { chainId: this.chainId }, safeCause);
|
|
540
1057
|
}
|
|
541
1058
|
/**
|
|
542
1059
|
* Resolves the token program owner for a given mint. Token-2022 mints are owned by
|
|
@@ -705,4 +1222,125 @@ class SolanaChain extends chain_base_ts_1.Chain {
|
|
|
705
1222
|
}
|
|
706
1223
|
}
|
|
707
1224
|
exports.SolanaChain = SolanaChain;
|
|
1225
|
+
function validateAltList(input, chainId) {
|
|
1226
|
+
if (input === undefined || input === null)
|
|
1227
|
+
return [];
|
|
1228
|
+
if (!Array.isArray(input)) {
|
|
1229
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.InvalidArgument, `addressLookupTables must be an array of AddressLookupTableAccount, got ${typeof input}`, { chainId });
|
|
1230
|
+
}
|
|
1231
|
+
for (let i = 0; i < input.length; i++) {
|
|
1232
|
+
const a = input[i];
|
|
1233
|
+
if (a === null || typeof a !== 'object' || !('key' in a) || !('state' in a)) {
|
|
1234
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.InvalidArgument, `addressLookupTables[${i}] is not an AddressLookupTableAccount (missing key/state). Use SolanaChain.fetchAddressLookupTable to obtain valid instances.`, { chainId });
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
return input;
|
|
1238
|
+
}
|
|
1239
|
+
function signatureBase58FromBytes(txBytes) {
|
|
1240
|
+
if (txBytes.length < 65) {
|
|
1241
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.InvalidArgument, `Solana broadcast: signed bytes too short (${txBytes.length}) to extract signature`, {});
|
|
1242
|
+
}
|
|
1243
|
+
const numSigs = txBytes[0];
|
|
1244
|
+
if (numSigs === 0 || txBytes.length < 1 + 64) {
|
|
1245
|
+
throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.InvalidArgument, `Solana broadcast: no signatures in serialized transaction`, {});
|
|
1246
|
+
}
|
|
1247
|
+
const sigBytes = txBytes.subarray(1, 1 + 64);
|
|
1248
|
+
return bs58encode(sigBytes);
|
|
1249
|
+
}
|
|
1250
|
+
const BS58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
|
1251
|
+
function bs58encode(bytes) {
|
|
1252
|
+
let n = 0n;
|
|
1253
|
+
for (const b of bytes)
|
|
1254
|
+
n = (n << 8n) | BigInt(b);
|
|
1255
|
+
let out = '';
|
|
1256
|
+
while (n > 0n) {
|
|
1257
|
+
const rem = Number(n % 58n);
|
|
1258
|
+
out = BS58_ALPHABET[rem] + out;
|
|
1259
|
+
n = n / 58n;
|
|
1260
|
+
}
|
|
1261
|
+
for (const b of bytes) {
|
|
1262
|
+
if (b === 0)
|
|
1263
|
+
out = '1' + out;
|
|
1264
|
+
else
|
|
1265
|
+
break;
|
|
1266
|
+
}
|
|
1267
|
+
return out;
|
|
1268
|
+
}
|
|
1269
|
+
const SOLANA_BATCH_STATUS_CONCURRENCY = 8;
|
|
1270
|
+
async function runSolanaBatchStatus(items, fetchOne) {
|
|
1271
|
+
const results = new Array(items.length);
|
|
1272
|
+
const ac = new AbortController();
|
|
1273
|
+
let cursor = 0;
|
|
1274
|
+
const workers = [];
|
|
1275
|
+
const spawn = async () => {
|
|
1276
|
+
while (!ac.signal.aborted) {
|
|
1277
|
+
const idx = cursor++;
|
|
1278
|
+
if (idx >= items.length)
|
|
1279
|
+
return;
|
|
1280
|
+
try {
|
|
1281
|
+
results[idx] = await fetchOne(items[idx], ac.signal);
|
|
1282
|
+
}
|
|
1283
|
+
catch (err) {
|
|
1284
|
+
ac.abort();
|
|
1285
|
+
throw err;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
};
|
|
1289
|
+
const workerCount = Math.min(SOLANA_BATCH_STATUS_CONCURRENCY, items.length);
|
|
1290
|
+
for (let i = 0; i < workerCount; i++)
|
|
1291
|
+
workers.push(spawn());
|
|
1292
|
+
await Promise.all(workers);
|
|
1293
|
+
return results;
|
|
1294
|
+
}
|
|
1295
|
+
function anySignal(a, b) {
|
|
1296
|
+
if (typeof AbortSignal.any === 'function')
|
|
1297
|
+
return AbortSignal.any([a, b]);
|
|
1298
|
+
const ac = new AbortController();
|
|
1299
|
+
const onAbort = () => ac.abort();
|
|
1300
|
+
if (a.aborted || b.aborted)
|
|
1301
|
+
ac.abort();
|
|
1302
|
+
else {
|
|
1303
|
+
a.addEventListener('abort', onAbort, { once: true });
|
|
1304
|
+
b.addEventListener('abort', onAbort, { once: true });
|
|
1305
|
+
}
|
|
1306
|
+
return ac.signal;
|
|
1307
|
+
}
|
|
1308
|
+
function combineJitoSignal(consumerSignal, timeoutMs) {
|
|
1309
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
1310
|
+
if (!consumerSignal)
|
|
1311
|
+
return timeout;
|
|
1312
|
+
const ac = new AbortController();
|
|
1313
|
+
const onAbort = () => ac.abort();
|
|
1314
|
+
if (consumerSignal.aborted || timeout.aborted)
|
|
1315
|
+
ac.abort();
|
|
1316
|
+
else {
|
|
1317
|
+
consumerSignal.addEventListener('abort', onAbort, { once: true });
|
|
1318
|
+
timeout.addEventListener('abort', onAbort, { once: true });
|
|
1319
|
+
}
|
|
1320
|
+
return ac.signal;
|
|
1321
|
+
}
|
|
1322
|
+
async function solanaInterruptibleSleep(ms, signal) {
|
|
1323
|
+
if (signal?.aborted)
|
|
1324
|
+
return;
|
|
1325
|
+
await new Promise((resolve) => {
|
|
1326
|
+
const t = setTimeout(() => {
|
|
1327
|
+
signal?.removeEventListener('abort', onAbort);
|
|
1328
|
+
resolve();
|
|
1329
|
+
}, ms);
|
|
1330
|
+
const onAbort = () => {
|
|
1331
|
+
clearTimeout(t);
|
|
1332
|
+
resolve();
|
|
1333
|
+
};
|
|
1334
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
function toAccountInfoResult(info) {
|
|
1338
|
+
return {
|
|
1339
|
+
owner: info.owner.toBase58(),
|
|
1340
|
+
lamports: BigInt(info.lamports),
|
|
1341
|
+
data: Uint8Array.from(info.data),
|
|
1342
|
+
executable: info.executable,
|
|
1343
|
+
rentEpoch: info.rentEpoch === undefined ? undefined : BigInt(info.rentEpoch),
|
|
1344
|
+
};
|
|
1345
|
+
}
|
|
708
1346
|
//# sourceMappingURL=solana_chain.js.map
|