@usearete/adapter-kit 0.16.0 → 0.18.0

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/dist/index.js CHANGED
@@ -1,5 +1,110 @@
1
- import { AccountRole, address, pipe, createTransactionMessage, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, appendTransactionMessageInstructions, addSignersToTransactionMessage, signTransactionMessageWithSigners, getSignatureFromTransaction, getBase64EncodedWireTransaction, sendAndConfirmTransactionFactory, isSolanaError, SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE, setTransactionMessageFeePayer, compileTransaction, getBase64Decoder } from '@solana/kit';
2
- import { resolveTransactionBuildOptions, TransactionTransportError } from '@usearete/sdk';
1
+ import { getTransactionVersionDecoder, getTransactionEncoder, getTransactionDecoder, AccountRole, address, getTransactionSize, getBase64EncodedWireTransaction, compileTransaction, signTransactionMessageWithSigners, assertIsTransactionWithBlockhashLifetime, getSignatureFromTransaction, sendAndConfirmTransactionFactory, isSolanaError, SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE, createNoopSigner, getBase64Decoder, addSignersToTransactionMessage, setTransactionMessageConfig, createTransactionMessage, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, appendTransactionMessageInstructions, setTransactionMessageComputeUnitLimit, setTransactionMessageComputeUnitPrice, setTransactionMessageHeapSize, setTransactionMessageLoadedAccountsDataSizeLimit } from '@solana/kit';
2
+ import { resolveTransactionBuildOptions, TransactionTransportError, toWireResourceOptions } from '@usearete/sdk';
3
+
4
+ /**
5
+ * Wallet Standard → kit signer bridge.
6
+ *
7
+ * The handoff is byte-oriented on purpose: the app picks the transaction
8
+ * format, the wallet decodes the version out of the bytes it is given, and
9
+ * Arete forwards whatever came back without converting versions. Nothing
10
+ * here routes a transaction through a web3.js 1.x wrapper, which cannot
11
+ * represent V1 at all.
12
+ *
13
+ * The capability check is a local fact about the wallet, not proof the
14
+ * cluster has activated a version: the account must advertise
15
+ * `solana:signTransaction`, and *that* feature must advertise the version
16
+ * being signed. Sign-and-send support is never read as sign-only support —
17
+ * a wallet that can only sign-and-send cannot hand bytes back for Arete to
18
+ * relay.
19
+ *
20
+ * As of Kit 8, the upstream Wallet Standard `signTransaction` feature still
21
+ * types `supportedTransactionVersions` as `legacy | 0`. The interfaces below
22
+ * are therefore declared structurally rather than imported, so a wallet that
23
+ * does advertise `1` is usable without a cast and one that does not is
24
+ * refused before it is ever prompted.
25
+ */
26
+ /** The feature name a sign-only handoff requires. */
27
+ const SOLANA_SIGN_TRANSACTION = 'solana:signTransaction';
28
+ /** The feature name that is explicitly *not* evidence of sign-only support. */
29
+ const SOLANA_SIGN_AND_SEND_TRANSACTION = 'solana:signAndSendTransaction';
30
+ /** Typed refusal from the Wallet Standard bridge. */
31
+ class WalletStandardSignerError extends Error {
32
+ constructor(code, message) {
33
+ super(message);
34
+ this.name = 'WalletStandardSignerError';
35
+ this.code = code;
36
+ }
37
+ }
38
+ function readSignTransactionFeature(config) {
39
+ if (!config.account.features.includes(SOLANA_SIGN_TRANSACTION)) {
40
+ const signAndSendOnly = config.account.features.includes(SOLANA_SIGN_AND_SEND_TRANSACTION);
41
+ throw new WalletStandardSignerError('sign_transaction_unsupported', `Account ${config.account.address} does not support '${SOLANA_SIGN_TRANSACTION}'`
42
+ + (signAndSendOnly
43
+ ? `; it advertises only '${SOLANA_SIGN_AND_SEND_TRANSACTION}', which cannot return `
44
+ + 'signed bytes for Arete to relay'
45
+ : ''));
46
+ }
47
+ const feature = config.wallet.features[SOLANA_SIGN_TRANSACTION];
48
+ if (!feature || typeof feature.signTransaction !== 'function') {
49
+ throw new WalletStandardSignerError('sign_transaction_unsupported', `Wallet does not implement the '${SOLANA_SIGN_TRANSACTION}' feature`);
50
+ }
51
+ return feature;
52
+ }
53
+ /**
54
+ * A kit {@link TransactionSigner} backed by a Wallet Standard account.
55
+ *
56
+ * The feature is resolved once, at creation, so a wallet that cannot sign at
57
+ * all fails before any transaction is built. The per-version check happens
58
+ * per transaction, against the version decoded from the bytes about to be
59
+ * handed over — so a v0-only wallet refuses a V1 transaction without ever
60
+ * showing the user a prompt.
61
+ */
62
+ function createWalletStandardSigner(config) {
63
+ const feature = readSignTransactionFeature(config);
64
+ const address = config.account.address;
65
+ return {
66
+ address,
67
+ async signTransactions(transactions) {
68
+ if (transactions.length === 0)
69
+ return [];
70
+ const inputs = transactions.map((transaction) => {
71
+ const version = getTransactionVersionDecoder().decode(transaction.messageBytes);
72
+ if (!feature.supportedTransactionVersions.includes(version)) {
73
+ throw new WalletStandardSignerError('transaction_version_unsupported', `Wallet does not support signing transaction version ${JSON.stringify(version)} `
74
+ + `(supported: ${feature.supportedTransactionVersions
75
+ .map((supported) => JSON.stringify(supported))
76
+ .join(', ')})`);
77
+ }
78
+ return {
79
+ account: config.account,
80
+ chain: config.chain,
81
+ transaction: getTransactionEncoder().encode(transaction),
82
+ };
83
+ });
84
+ const outputs = await feature.signTransaction(...inputs);
85
+ if (outputs.length !== transactions.length) {
86
+ throw new WalletStandardSignerError('wallet_returned_invalid_transaction', `Wallet returned ${outputs.length} signed transactions for ${transactions.length} `
87
+ + 'inputs');
88
+ }
89
+ return outputs.map((output, index) => {
90
+ const returned = getTransactionDecoder().decode(output.signedTransaction);
91
+ // A wallet may add signatures; it may not rewrite the message the
92
+ // app compiled. Anything else and the bytes Arete would relay are
93
+ // not the bytes the caller approved.
94
+ const original = transactions[index].messageBytes;
95
+ if (returned.messageBytes.length !== original.length
96
+ || returned.messageBytes.some((byte, offset) => byte !== original[offset])) {
97
+ throw new WalletStandardSignerError('wallet_returned_invalid_transaction', 'Wallet returned a transaction whose message differs from the one it was given');
98
+ }
99
+ const signature = returned.signatures[address];
100
+ if (!signature) {
101
+ throw new WalletStandardSignerError('wallet_returned_invalid_transaction', `Wallet returned no signature for ${address}`);
102
+ }
103
+ return { [address]: signature };
104
+ });
105
+ },
106
+ };
107
+ }
3
108
 
4
109
  /**
5
110
  * @usearete/adapter-kit
@@ -12,13 +117,58 @@ import { resolveTransactionBuildOptions, TransactionTransportError } from '@usea
12
117
  * sending, and confirmation.
13
118
  */
14
119
  /**
15
- * Kit builds v0 messages only, and applies no compute-budget instructions of
16
- * its own: transaction V1 and the resource budget options land with A4-253.
120
+ * Kit 8 is the first release that constructs transaction V1 messages and
121
+ * carries the resource budget as a typed message config, so this adapter
122
+ * builds every version the contract defines and honours every typed
123
+ * resource option.
17
124
  */
18
125
  const CAPABILITY = {
19
- supportedTransactionVersions: [0],
20
- supportedResourceOptions: [],
126
+ supportedTransactionVersions: ['legacy', 0, 1],
127
+ supportedResourceOptions: [
128
+ 'computeUnitLimit',
129
+ 'loadedAccountsDataSizeLimit',
130
+ 'heapSize',
131
+ 'priorityFeeLamports',
132
+ 'computeUnitPriceMicroLamports',
133
+ ],
21
134
  };
135
+ /** Wire ceiling for legacy and v0 transactions, in bytes (one UDP packet). */
136
+ const LEGACY_TRANSACTION_SIZE_LIMIT = 1232;
137
+ /** Wire ceiling for V1 transactions, in bytes (SIMD-0385). */
138
+ const V1_TRANSACTION_SIZE_LIMIT = 4096;
139
+ /** Structural V1 caps enforced before a signer is reached (SIMD-0385). */
140
+ const V1_MAX_SIGNATURES = 12;
141
+ const V1_MAX_ACCOUNTS = 64;
142
+ const V1_MAX_INSTRUCTIONS = 64;
143
+ /**
144
+ * Per-transaction compute-unit maximum. Also the limit a *provisional* V1
145
+ * message declares for a budget the caller omitted: SIMD-0385 makes an unset
146
+ * V1 config field request the **minimum**, not a default, so a provisional
147
+ * message carrying nothing could never produce an ordinary successful
148
+ * execution estimate.
149
+ */
150
+ const MAX_COMPUTE_UNIT_LIMIT = 1400000;
151
+ /** Loaded-account-data maximum (64 MiB), used the same way. */
152
+ const MAX_LOADED_ACCOUNTS_DATA_SIZE = 64 * 1024 * 1024;
153
+ /**
154
+ * Headroom added to measured compute consumption, in percent: a simulation
155
+ * is one slot's view of the chain and the transaction that lands may take a
156
+ * slightly different branch.
157
+ */
158
+ const ESTIMATED_COMPUTE_UNIT_HEADROOM_PERCENT = 20n;
159
+ /**
160
+ * The runtime accounts for loaded data in pages of this size, so an
161
+ * estimated data budget is rounded up to a whole page plus one page of
162
+ * headroom for account growth between simulation and execution.
163
+ */
164
+ const LOADED_ACCOUNTS_DATA_PAGE_BYTES = 32 * 1024;
165
+ const COMPUTE_BUDGET_PROGRAM_ADDRESS = 'ComputeBudget111111111111111111111111111111';
166
+ /** Option keys that would carry address lookup tables. */
167
+ const LOOKUP_TABLE_OPTION_KEYS = [
168
+ 'addressLookupTables',
169
+ 'addressLookupTableAccounts',
170
+ 'lookupTables',
171
+ ];
22
172
  /** Structured adapter error consumed by the Arete transaction outcome APIs. */
23
173
  class KitTransactionExecutionError extends Error {
24
174
  constructor(outcome) {
@@ -91,14 +241,72 @@ function resolveTransport(selection, rpc, rpcSubscriptions, context) {
91
241
  return { kind: 'direct', rpc, rpcSubscriptions };
92
242
  throw new Error('No transaction transport is available; connect through Arete or configure direct RPC');
93
243
  }
94
- async function pollAreteStatus(transport, signature, commitment, lastValidBlockHeight, options) {
95
- const deadline = Date.now() + (options?.confirmationTimeoutMs ?? 60000);
244
+ /**
245
+ * Report, never adopt, a relay signature that is not the one signed here.
246
+ *
247
+ * The adapter signs the final bytes and derives the signature from them, so
248
+ * the local one is authoritative. Reconciling an echoed signature would
249
+ * poll a transaction this adapter never submitted: it can report another
250
+ * transaction's status as this one's, or never confirm the one that went
251
+ * out.
252
+ */
253
+ function reportSignatureMismatch(local, echoed) {
254
+ if (echoed && echoed !== local) {
255
+ // eslint-disable-next-line no-console -- the adapter has no logger, and
256
+ // this runs after the transaction may already be on the wire, so it
257
+ // must not interrupt the signature-bearing outcome.
258
+ console.warn(`Arete relay reported signature ${echoed} for a transaction signed as ${local}; `
259
+ + 'the locally derived signature is authoritative and is the one being reconciled');
260
+ }
261
+ }
262
+ /** Thrown by {@link withDeadline} when the shared deadline passes first. */
263
+ class DeadlineExpiredError extends Error {
264
+ constructor() {
265
+ super('The configured confirmation deadline expired');
266
+ this.name = 'DeadlineExpiredError';
267
+ }
268
+ }
269
+ const DEADLINE_EXPIRED = Symbol('deadline-expired');
270
+ /**
271
+ * Await `work`, or give up at `deadline`.
272
+ *
273
+ * A racing timer rather than an abort signal: `TransactionTransport` takes
274
+ * none, and no HTTP client under it imposes a request timeout, so a backend
275
+ * that stops answering would otherwise hold the caller forever. The
276
+ * abandoned request is left with a no-op rejection handler — it may still be
277
+ * in flight, which is exactly why the outcome is *unknown* rather than
278
+ * failed.
279
+ */
280
+ async function withDeadline(deadline, work) {
281
+ work.catch(() => { });
282
+ let timer;
283
+ // Executor form, not `Promise.withResolvers`: that landed in Node 22 and
284
+ // this package's floor is the Node 20.18 that Kit 8 requires.
285
+ const expiry = new Promise((resolve) => {
286
+ timer = setTimeout(() => resolve(DEADLINE_EXPIRED), Math.max(deadline - Date.now(), 0));
287
+ });
288
+ try {
289
+ const result = await Promise.race([work, expiry]);
290
+ if (result === DEADLINE_EXPIRED)
291
+ throw new DeadlineExpiredError();
292
+ return result;
293
+ }
294
+ finally {
295
+ clearTimeout(timer);
296
+ }
297
+ }
298
+ /**
299
+ * Poll until the signature reaches `commitment`, its lifetime expires, or
300
+ * `deadline` passes — including while a request is in flight. Never
301
+ * resubmits, and only ever polls the signature the caller signed.
302
+ */
303
+ async function pollAreteStatus(transport, signature, commitment, lastValidBlockHeight, deadline, options) {
96
304
  const interval = options?.statusPollIntervalMs ?? 500;
97
305
  let emptyStatusPolls = 0;
98
306
  while (Date.now() <= deadline) {
99
- const status = await transport.getSignatureStatus(signature, {
307
+ const status = await withDeadline(deadline, transport.getSignatureStatus(signature, {
100
308
  commitment, searchTransactionHistory: true,
101
- });
309
+ }));
102
310
  if (status?.err) {
103
311
  throw new KitTransactionExecutionError({
104
312
  status: 'chain-failed', phase: 'confirmation', signature,
@@ -108,7 +316,8 @@ async function pollAreteStatus(transport, signature, commitment, lastValidBlockH
108
316
  if (status && hasReachedCommitment(status.confirmationStatus, commitment)) {
109
317
  return { signature, slot: status.slot === null ? undefined : Number(status.slot) };
110
318
  }
111
- if (await transport.getBlockHeight({ commitment }) > lastValidBlockHeight) {
319
+ if (await withDeadline(deadline, transport.getBlockHeight({ commitment }))
320
+ > lastValidBlockHeight) {
112
321
  throw new KitTransactionExecutionError({
113
322
  status: 'submitted-unknown', phase: 'confirmation', signature,
114
323
  slot: status?.slot === null ? undefined : Number(status?.slot),
@@ -116,7 +325,8 @@ async function pollAreteStatus(transport, signature, commitment, lastValidBlockH
116
325
  });
117
326
  }
118
327
  emptyStatusPolls = status ? 0 : emptyStatusPolls + 1;
119
- const delayMs = Math.min(interval * (2 ** Math.min(emptyStatusPolls, 3)), 4000);
328
+ const backoffMs = Math.min(interval * (2 ** Math.min(emptyStatusPolls, 3)), 4000);
329
+ const delayMs = Math.min(backoffMs, Math.max(deadline - Date.now(), 0));
120
330
  await new Promise((resolve) => setTimeout(resolve, delayMs));
121
331
  }
122
332
  throw new KitTransactionExecutionError({
@@ -174,7 +384,7 @@ function fromAccountRole(role) {
174
384
  return { isSigner: false, isWritable: false };
175
385
  }
176
386
  }
177
- /** Convert an Arete BuiltInstruction to a kit IInstruction. */
387
+ /** Convert an Arete BuiltInstruction to a kit Instruction. */
178
388
  function toKitInstruction(ix) {
179
389
  const accounts = ix.keys.map((k) => ({
180
390
  address: address(k.pubkey),
@@ -186,7 +396,7 @@ function toKitInstruction(ix) {
186
396
  data: ix.data,
187
397
  };
188
398
  }
189
- /** Convert a kit IInstruction to an Arete BuiltInstruction. */
399
+ /** Convert a kit Instruction to an Arete BuiltInstruction. */
190
400
  function fromKitInstruction(ix) {
191
401
  return {
192
402
  programId: ix.programAddress,
@@ -197,6 +407,232 @@ function fromKitInstruction(ix) {
197
407
  data: ix.data ? new Uint8Array(ix.data) : new Uint8Array(0),
198
408
  };
199
409
  }
410
+ // ---------------------------------------------------------------------------
411
+ // One version/configuration planner, shared by send and unsigned inspection
412
+ // ---------------------------------------------------------------------------
413
+ /**
414
+ * Refuse the inputs the typed configuration owns.
415
+ *
416
+ * A hand-built `ComputeBudget` instruction is not an effective V1
417
+ * configuration — the runtime reads V1 budgets from the message config — and
418
+ * silently accepting one would apply a budget on legacy/v0 and none on V1
419
+ * from the same caller code. Address lookup tables have no V1 encoding at
420
+ * all, and this adapter compiles none for any version.
421
+ */
422
+ function assertBuildableInputs(instructions, options, version) {
423
+ if (instructions.length === 0) {
424
+ throw new Error('A transaction requires at least one instruction');
425
+ }
426
+ if (instructions.some((ix) => ix.programId === COMPUTE_BUDGET_PROGRAM_ADDRESS)) {
427
+ throw new Error('Caller-supplied ComputeBudget instructions are rejected: request compute units, heap '
428
+ + 'size, loaded-accounts size and fees through the typed `resources` options so one '
429
+ + 'contract covers every transaction version');
430
+ }
431
+ const lookupTableKey = LOOKUP_TABLE_OPTION_KEYS.find((key) => options?.[key] !== undefined);
432
+ if (lookupTableKey) {
433
+ throw new Error(`Address lookup tables are not supported by this adapter (rejected option `
434
+ + `'${lookupTableKey}')`
435
+ + (version === 1 ? '; transaction version 1 has no lookup tables at all (SIMD-0385)' : ''));
436
+ }
437
+ }
438
+ function toUnits(value) {
439
+ return value === undefined ? undefined : Number(value);
440
+ }
441
+ /**
442
+ * A V1 message: the resource budget is the message's typed config, and the
443
+ * runtime reads it from there — which is why a caller-supplied
444
+ * `ComputeBudget` instruction is rejected rather than merged.
445
+ */
446
+ function planV1Message(input) {
447
+ const { resources } = input;
448
+ const configured = setTransactionMessageConfig({
449
+ computeUnitLimit: toUnits(resources.computeUnitLimit),
450
+ heapSize: toUnits(resources.heapSize),
451
+ loadedAccountsDataSizeLimit: toUnits(resources.loadedAccountsDataSizeLimit),
452
+ priorityFeeLamports: resources.priorityFeeLamports,
453
+ }, createTransactionMessage({ version: 1 }));
454
+ const withFeePayer = setTransactionMessageFeePayerSigner(input.feePayer, configured);
455
+ const withLifetime = setTransactionMessageLifetimeUsingBlockhash(input.latestBlockhash, withFeePayer);
456
+ return appendTransactionMessageInstructions(input.instructions.map(toKitInstruction), withLifetime);
457
+ }
458
+ /**
459
+ * A legacy or v0 message: the same budget, rendered as the canonical
460
+ * `ComputeBudget` instructions kit prepends.
461
+ */
462
+ function planLegacyMessage(input) {
463
+ const { resources } = input;
464
+ const created = input.version === 0
465
+ ? createTransactionMessage({ version: 0 })
466
+ : createTransactionMessage({ version: 'legacy' });
467
+ const withFeePayer = setTransactionMessageFeePayerSigner(input.feePayer, created);
468
+ const withLifetime = setTransactionMessageLifetimeUsingBlockhash(input.latestBlockhash, withFeePayer);
469
+ let message = appendTransactionMessageInstructions(input.instructions.map(toKitInstruction), withLifetime);
470
+ if (resources.computeUnitLimit !== undefined) {
471
+ message = setTransactionMessageComputeUnitLimit(Number(resources.computeUnitLimit), message);
472
+ }
473
+ if (resources.computeUnitPriceMicroLamports !== undefined) {
474
+ message = setTransactionMessageComputeUnitPrice(resources.computeUnitPriceMicroLamports, message);
475
+ }
476
+ if (resources.heapSize !== undefined) {
477
+ message = setTransactionMessageHeapSize(Number(resources.heapSize), message);
478
+ }
479
+ if (resources.loadedAccountsDataSizeLimit !== undefined) {
480
+ message = setTransactionMessageLoadedAccountsDataSizeLimit(Number(resources.loadedAccountsDataSizeLimit), message);
481
+ }
482
+ return message;
483
+ }
484
+ /**
485
+ * One version/configuration planner for send and unsigned inspection: the
486
+ * only difference between the two is whether the fee payer can sign.
487
+ */
488
+ function planMessage(input) {
489
+ const message = input.version === 1 ? planV1Message(input) : planLegacyMessage(input);
490
+ return input.attachedSigners?.length
491
+ ? addSignersToTransactionMessage([...input.attachedSigners], message)
492
+ : message;
493
+ }
494
+ /**
495
+ * The version's wire ceiling, checked on the compiled bytes before a signer
496
+ * is ever reached.
497
+ *
498
+ * The V1 structural caps ({@link V1_MAX_SIGNATURES} signatures,
499
+ * {@link V1_MAX_ACCOUNTS} accounts, {@link V1_MAX_INSTRUCTIONS} top-level
500
+ * instructions) are enforced by `compileTransaction` itself, so they fail
501
+ * here too — one compile earlier than this check, and equally before
502
+ * signing. Re-implementing them would only risk disagreeing with the codec.
503
+ */
504
+ function assertWithinSizeLimit(version, size) {
505
+ const limit = version === 1 ? V1_TRANSACTION_SIZE_LIMIT : LEGACY_TRANSACTION_SIZE_LIMIT;
506
+ if (size > limit) {
507
+ throw new Error(`Transaction is ${size} bytes, over the ${limit}-byte limit for version `
508
+ + `${JSON.stringify(version)}`);
509
+ }
510
+ }
511
+ /** Measured consumption plus headroom, positive and protocol-bounded. */
512
+ function estimatedComputeUnitLimit(unitsConsumed) {
513
+ const padded = (unitsConsumed * (100n + ESTIMATED_COMPUTE_UNIT_HEADROOM_PERCENT) + 99n) / 100n;
514
+ if (padded < 1n)
515
+ return 1n;
516
+ return padded > BigInt(MAX_COMPUTE_UNIT_LIMIT) ? BigInt(MAX_COMPUTE_UNIT_LIMIT) : padded;
517
+ }
518
+ /** Measured loaded data rounded up to a whole page plus one page of headroom. */
519
+ function estimatedLoadedAccountsDataSize(loadedBytes) {
520
+ const page = BigInt(LOADED_ACCOUNTS_DATA_PAGE_BYTES);
521
+ const padded = ((loadedBytes + page - 1n) / page + 1n) * page;
522
+ const max = BigInt(MAX_LOADED_ACCOUNTS_DATA_SIZE);
523
+ return padded > max ? max : padded;
524
+ }
525
+ async function fetchLatestBlockhash(resolved, commitment, minContextSlot) {
526
+ const value = resolved.kind === 'arete'
527
+ ? await resolved.transport.getLatestBlockhash({ commitment, minContextSlot })
528
+ : (await resolved.rpc.getLatestBlockhash({ commitment, minContextSlot }).send()).value;
529
+ return {
530
+ blockhash: value.blockhash,
531
+ lastValidBlockHeight: BigInt(value.lastValidBlockHeight),
532
+ };
533
+ }
534
+ /**
535
+ * Simulate a transaction carrying placeholder signatures. Verification is
536
+ * off on both backends: nothing on this path has touched a signer.
537
+ */
538
+ async function simulateUnsigned(resolved, wireTransaction, commitment, minContextSlot) {
539
+ if (resolved.kind === 'arete') {
540
+ const simulation = await resolved.transport.simulateTransaction(wireTransaction, {
541
+ commitment,
542
+ minContextSlot,
543
+ });
544
+ return {
545
+ contextSlot: simulation.contextSlot,
546
+ err: simulation.err ?? undefined,
547
+ logs: simulation.logs ?? undefined,
548
+ unitsConsumed: simulation.unitsConsumed,
549
+ loadedAccountsDataSize: simulation.loadedAccountsDataSize,
550
+ };
551
+ }
552
+ const simulation = await resolved.rpc.simulateTransaction(wireTransaction, { commitment, encoding: 'base64', minContextSlot, sigVerify: false }).send();
553
+ // The loaded-accounts budget is optional upstream and comes back as null
554
+ // when the node did not measure it; `Number(null)` would invent a measured
555
+ // zero and destroy the distinction estimation depends on.
556
+ const { loadedAccountsDataSize } = simulation.value;
557
+ return {
558
+ contextSlot: simulation.context.slot,
559
+ err: simulation.value.err ?? undefined,
560
+ logs: simulation.value.logs ?? undefined,
561
+ unitsConsumed: simulation.value.unitsConsumed == null
562
+ ? undefined
563
+ : BigInt(simulation.value.unitsConsumed),
564
+ loadedAccountsDataSize: loadedAccountsDataSize == null
565
+ ? undefined
566
+ : BigInt(loadedAccountsDataSize),
567
+ };
568
+ }
569
+ async function estimateFee(resolved, encodedMessage, commitment, minContextSlot) {
570
+ if (resolved.kind === 'arete') {
571
+ const fee = await resolved.transport.getFeeForMessage(encodedMessage, {
572
+ commitment,
573
+ minContextSlot,
574
+ });
575
+ return {
576
+ feeLamports: fee.feeLamports === null ? undefined : fee.feeLamports,
577
+ contextSlot: fee.contextSlot,
578
+ };
579
+ }
580
+ const fee = await resolved.rpc
581
+ .getFeeForMessage(encodedMessage, { commitment, minContextSlot })
582
+ .send();
583
+ return {
584
+ feeLamports: fee.value === null ? undefined : fee.value,
585
+ contextSlot: fee.context.slot,
586
+ };
587
+ }
588
+ /**
589
+ * The V1 message a simulation can actually execute: the caller's own budgets
590
+ * where they gave them, the protocol maxima where they did not.
591
+ */
592
+ function provisionalV1Resources(resources) {
593
+ return {
594
+ ...resources,
595
+ computeUnitLimit: resources.computeUnitLimit ?? BigInt(MAX_COMPUTE_UNIT_LIMIT),
596
+ loadedAccountsDataSizeLimit: resources.loadedAccountsDataSizeLimit ?? BigInt(MAX_LOADED_ACCOUNTS_DATA_SIZE),
597
+ };
598
+ }
599
+ /**
600
+ * Resolve the two budgets a signable V1 message must declare.
601
+ *
602
+ * An unset V1 budget requests the *minimum* rather than a default
603
+ * (SIMD-0385), so a final message missing either could only fail on chain.
604
+ * An explicit caller budget is used verbatim and never raised; a missing one
605
+ * is measured by simulating the provisional message and derived with
606
+ * headroom; and a metric the simulation never reported is refused by name.
607
+ */
608
+ async function resolveV1Budgets(resolved, compileProvisional, resources, commitment, minContextSlot) {
609
+ if (resources.computeUnitLimit !== undefined
610
+ && resources.loadedAccountsDataSizeLimit !== undefined) {
611
+ return resources;
612
+ }
613
+ const simulation = await simulateUnsigned(resolved, compileProvisional(provisionalV1Resources(resources)), commitment, minContextSlot);
614
+ if (simulation.err) {
615
+ throw new Error(`Budget estimation simulation reported ${JSON.stringify(simulation.err)}; `
616
+ + 'nothing was submitted');
617
+ }
618
+ const unestimable = (option, metric) => new Error(`${option} is required for transaction version 1 and could not be estimated: the `
619
+ + `simulation reported no ${metric}. Pass an explicit ${option}, or use a backend whose `
620
+ + `simulation reports ${metric}.`);
621
+ let { computeUnitLimit, loadedAccountsDataSizeLimit } = resources;
622
+ if (computeUnitLimit === undefined) {
623
+ if (simulation.unitsConsumed === undefined) {
624
+ throw unestimable('computeUnitLimit', 'unitsConsumed');
625
+ }
626
+ computeUnitLimit = estimatedComputeUnitLimit(simulation.unitsConsumed);
627
+ }
628
+ if (loadedAccountsDataSizeLimit === undefined) {
629
+ if (simulation.loadedAccountsDataSize === undefined) {
630
+ throw unestimable('loadedAccountsDataSizeLimit', 'loadedAccountsDataSize');
631
+ }
632
+ loadedAccountsDataSizeLimit = estimatedLoadedAccountsDataSize(simulation.loadedAccountsDataSize);
633
+ }
634
+ return { ...resources, computeUnitLimit, loadedAccountsDataSizeLimit };
635
+ }
200
636
  /**
201
637
  * Create a {@link WalletAdapter} from a kit RPC pair and a signer.
202
638
  */
@@ -210,17 +646,21 @@ function createWalletAdapter(config) {
210
646
  signerAddresses: [...new Set(signerAddresses)],
211
647
  supportedTransactionVersions: CAPABILITY.supportedTransactionVersions,
212
648
  async signAndSend(instructions, options, context) {
213
- // Rejects an explicit non-v0 version and any resource option this
214
- // adapter cannot apply, before a wallet is ever prompted.
215
- resolveTransactionBuildOptions(options, CAPABILITY);
216
- if (instructions.length === 0) {
217
- const cause = new Error('signAndSend requires at least one instruction');
649
+ // Rejects an unsupported explicit version and any version-bound fee
650
+ // used against the wrong version, before a wallet is ever prompted.
651
+ let plan;
652
+ try {
653
+ plan = resolveTransactionBuildOptions(options, CAPABILITY);
654
+ assertBuildableInputs(instructions, options, plan.transactionVersion);
655
+ }
656
+ catch (cause) {
218
657
  throw new KitTransactionExecutionError({
219
658
  status: 'not-submitted',
220
659
  phase: 'build',
221
660
  cause,
222
661
  });
223
662
  }
663
+ const version = plan.transactionVersion;
224
664
  const sendOptions = options;
225
665
  const feePayer = sendOptions?.feePayer ?? signer;
226
666
  const commitment = toCommitment(options?.confirmationLevel, fallbackCommitment);
@@ -249,12 +689,30 @@ function createWalletAdapter(config) {
249
689
  const attachedSigners = [...requiredSignerAddresses]
250
690
  .filter((requiredAddress) => requiredAddress !== feePayer.address)
251
691
  .map((requiredAddress) => localSignerMap.get(requiredAddress));
252
- const latestBlockhash = resolved.kind === 'arete'
253
- ? await resolved.transport.getLatestBlockhash({ commitment })
254
- : (await resolved.rpc.getLatestBlockhash({ commitment }).send()).value;
255
- lastValidBlockHeight = BigInt(latestBlockhash.lastValidBlockHeight);
256
- const messageWithFeePayer = pipe(createTransactionMessage({ version: 0 }), (m) => setTransactionMessageFeePayerSigner(feePayer, m), (m) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m), (m) => appendTransactionMessageInstructions(instructions.map(toKitInstruction), m));
257
- message = addSignersToTransactionMessage(attachedSigners, messageWithFeePayer);
692
+ const latestBlockhash = await fetchLatestBlockhash(resolved, commitment);
693
+ lastValidBlockHeight = latestBlockhash.lastValidBlockHeight;
694
+ const compile = (resources) => planMessage({
695
+ version,
696
+ resources,
697
+ instructions,
698
+ latestBlockhash,
699
+ feePayer,
700
+ attachedSigners,
701
+ });
702
+ // The budgets are resolved before the final message exists, so the
703
+ // configuration that is compiled, sized, preflighted and signed is
704
+ // one configuration. The provisional message is sized too: an
705
+ // oversized payload is a build failure with an exact byte count,
706
+ // not a backend-specific simulation error.
707
+ const encodeSized = (compiled) => {
708
+ assertWithinSizeLimit(version, getTransactionSize(compiled));
709
+ return getBase64EncodedWireTransaction(compiled);
710
+ };
711
+ const resources = version === 1
712
+ ? await resolveV1Budgets(resolved, (provisional) => encodeSized(compileTransaction(compile(provisional))), plan.resources, commitment)
713
+ : plan.resources;
714
+ message = compile(resources);
715
+ assertWithinSizeLimit(version, getTransactionSize(compileTransaction(message)));
258
716
  }
259
717
  catch (cause) {
260
718
  throw new KitTransactionExecutionError({
@@ -267,6 +725,7 @@ function createWalletAdapter(config) {
267
725
  let signature;
268
726
  try {
269
727
  signedTransaction = await signTransactionMessageWithSigners(message);
728
+ assertIsTransactionWithBlockhashLifetime(signedTransaction);
270
729
  signature = getSignatureFromTransaction(signedTransaction);
271
730
  }
272
731
  catch (cause) {
@@ -277,31 +736,45 @@ function createWalletAdapter(config) {
277
736
  });
278
737
  }
279
738
  if (resolved.kind === 'arete') {
280
- let submittedSignature;
739
+ // One deadline over submission *and* confirmation: a relay that
740
+ // takes the transaction and then stalls would otherwise hold the
741
+ // caller forever, because no HTTP client here imposes a request
742
+ // timeout. Expiry on either side is submitted-unknown under the
743
+ // signature these bytes carry — never a rebuild, re-sign or resend.
744
+ const deadline = Date.now() + (sendOptions?.confirmationTimeoutMs ?? 60000);
281
745
  try {
282
- const sent = await resolved.transport.sendTransaction(getBase64EncodedWireTransaction(signedTransaction), { skipPreflight: options?.skipPreflight ?? false, preflightCommitment: commitment });
283
- submittedSignature = sent.signature;
746
+ const sent = await withDeadline(deadline, resolved.transport.sendTransaction(getBase64EncodedWireTransaction(signedTransaction), { skipPreflight: options?.skipPreflight ?? false, preflightCommitment: commitment }));
747
+ reportSignatureMismatch(signature, sent.signature);
284
748
  }
285
749
  catch (cause) {
750
+ if (cause instanceof DeadlineExpiredError) {
751
+ throw new KitTransactionExecutionError({
752
+ status: 'submitted-unknown', phase: 'send', signature,
753
+ cause: new Error('The relay did not acknowledge the submission before the confirmation '
754
+ + 'deadline; the transaction was dispatched once and may still land'),
755
+ });
756
+ }
286
757
  if (cause instanceof TransactionTransportError && cause.submissionState === 'not_submitted') {
287
758
  throw new KitTransactionExecutionError({ status: 'not-submitted', phase: 'send', cause });
288
759
  }
760
+ if (cause instanceof TransactionTransportError) {
761
+ reportSignatureMismatch(signature, cause.signature);
762
+ }
763
+ // The locally derived signature stays authoritative: it is the one
764
+ // these bytes carry, so a relay reporting another identifies some
765
+ // other transaction and cannot be what the caller reconciles.
289
766
  throw new KitTransactionExecutionError({
290
- status: 'submitted-unknown', phase: 'send',
291
- signature: cause instanceof TransactionTransportError && cause.signature
292
- ? cause.signature : signature,
293
- cause,
767
+ status: 'submitted-unknown', phase: 'send', signature, cause,
294
768
  });
295
769
  }
296
770
  try {
297
- return await pollAreteStatus(resolved.transport, submittedSignature, commitment, lastValidBlockHeight, sendOptions);
771
+ return await pollAreteStatus(resolved.transport, signature, commitment, lastValidBlockHeight, deadline, sendOptions);
298
772
  }
299
773
  catch (cause) {
300
774
  if (cause instanceof KitTransactionExecutionError)
301
775
  throw cause;
302
776
  throw new KitTransactionExecutionError({
303
- status: 'submitted-unknown', phase: 'confirmation',
304
- signature: submittedSignature, cause,
777
+ status: 'submitted-unknown', phase: 'confirmation', signature, cause,
305
778
  });
306
779
  }
307
780
  }
@@ -375,68 +848,57 @@ function createWalletAdapter(config) {
375
848
  }
376
849
  },
377
850
  async inspectTransaction(instructions, options, context) {
378
- resolveTransactionBuildOptions(options, CAPABILITY);
379
- if (instructions.length === 0) {
380
- throw new Error('inspectTransaction requires at least one instruction');
381
- }
851
+ const plan = resolveTransactionBuildOptions(options, CAPABILITY);
852
+ const version = plan.transactionVersion;
853
+ assertBuildableInputs(instructions, options, version);
382
854
  const inspectionOptions = options;
383
855
  const commitment = inspectionOptions?.commitment ?? fallbackCommitment;
384
856
  const minContextSlot = inspectionOptions?.minContextSlot;
385
857
  const resolved = resolveTransport(config.transport, rpc, rpcSubscriptions, context);
386
- const latestBlockhash = resolved.kind === 'arete'
387
- ? await resolved.transport.getLatestBlockhash({ commitment, minContextSlot })
388
- : (await resolved.rpc.getLatestBlockhash({ commitment, minContextSlot }).send()).value;
389
- const message = pipe(createTransactionMessage({ version: 0 }), (m) => setTransactionMessageFeePayer(address(inspectionOptions?.feePayer ?? signer.address), m), (m) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m), (m) => appendTransactionMessageInstructions(instructions.map(toKitInstruction), m));
858
+ const latestBlockhash = await fetchLatestBlockhash(resolved, commitment, minContextSlot);
859
+ // Inspection is where you discover the budgets to pin, so an omitted
860
+ // V1 ceiling becomes the protocol maximum rather than the SIMD-0385
861
+ // minimum: that provisional message is what the reported metrics
862
+ // describe. Nothing here touches a signer — the fee payer is a bare
863
+ // address and the compiled transaction carries placeholder signatures.
864
+ const resources = version === 1 ? provisionalV1Resources(plan.resources) : plan.resources;
865
+ const message = planMessage({
866
+ version,
867
+ resources,
868
+ instructions,
869
+ latestBlockhash,
870
+ feePayer: createNoopSigner(address(inspectionOptions?.feePayer ?? signer.address)),
871
+ });
390
872
  const unsignedTransaction = compileTransaction(message);
873
+ // The same ceiling the send path enforces: a payload that could only
874
+ // be rejected on submission is refused here with its byte count,
875
+ // rather than answered with inspection results or a backend-specific
876
+ // simulation error.
877
+ assertWithinSizeLimit(version, getTransactionSize(unsignedTransaction));
391
878
  const wireTransaction = getBase64EncodedWireTransaction(unsignedTransaction);
392
879
  const encodedMessage = getBase64Decoder().decode(unsignedTransaction.messageBytes);
393
- if (resolved.kind === 'arete') {
394
- const [fee, simulation] = await Promise.all([
395
- resolved.transport.getFeeForMessage(encodedMessage, { commitment, minContextSlot }),
396
- resolved.transport.simulateTransaction(wireTransaction, { commitment, minContextSlot }),
397
- ]);
398
- return {
399
- feeLamports: fee.feeLamports === null ? undefined : toNumber(fee.feeLamports),
400
- logs: simulation.logs ?? undefined,
401
- computeUnitsConsumed: simulation.unitsConsumed === undefined
402
- ? undefined : toNumber(simulation.unitsConsumed),
403
- contextSlot: toNumber(simulation.contextSlot),
404
- error: simulation.err ?? undefined,
405
- loadedAccountsDataSize: simulation.loadedAccountsDataSize === undefined
406
- ? undefined : toNumber(simulation.loadedAccountsDataSize),
407
- feeContextSlot: toNumber(fee.contextSlot),
408
- };
409
- }
410
880
  const [fee, simulation] = await Promise.all([
411
- resolved.rpc.getFeeForMessage(encodedMessage, { commitment, minContextSlot }).send(),
412
- resolved.rpc.simulateTransaction(wireTransaction, {
413
- commitment,
414
- encoding: 'base64',
415
- minContextSlot,
416
- sigVerify: false,
417
- }).send(),
881
+ estimateFee(resolved, encodedMessage, commitment, minContextSlot),
882
+ simulateUnsigned(resolved, wireTransaction, commitment, minContextSlot),
418
883
  ]);
419
- // The RPC reports the loaded-accounts budget that @solana/kit 2.3 does
420
- // not yet type. It is optional upstream and comes back as null when the
421
- // node did not measure it — `Number(null)` would invent a measured zero
422
- // and destroy the missing-versus-zero distinction budget estimation
423
- // depends on.
424
- const { loadedAccountsDataSize: directLoadedAccountsDataSize } = simulation.value;
425
884
  return {
426
- feeLamports: fee.value === null ? undefined : toNumber(fee.value),
427
- logs: simulation.value.logs ?? undefined,
428
- computeUnitsConsumed: simulation.value.unitsConsumed == null
885
+ feeLamports: fee.feeLamports === undefined ? undefined : toNumber(fee.feeLamports),
886
+ logs: simulation.logs === undefined ? undefined : [...simulation.logs],
887
+ computeUnitsConsumed: simulation.unitsConsumed === undefined
888
+ ? undefined
889
+ : toNumber(simulation.unitsConsumed),
890
+ contextSlot: toNumber(simulation.contextSlot),
891
+ error: simulation.err ?? undefined,
892
+ loadedAccountsDataSize: simulation.loadedAccountsDataSize === undefined
429
893
  ? undefined
430
- : toNumber(simulation.value.unitsConsumed),
431
- contextSlot: toNumber(simulation.context.slot),
432
- error: simulation.value.err ?? undefined,
433
- loadedAccountsDataSize: directLoadedAccountsDataSize == null
434
- ? undefined : Number(directLoadedAccountsDataSize),
435
- feeContextSlot: toNumber(fee.context.slot),
894
+ : toNumber(simulation.loadedAccountsDataSize),
895
+ feeContextSlot: toNumber(fee.contextSlot),
896
+ transactionVersion: version,
897
+ resources: toWireResourceOptions(resources),
436
898
  };
437
899
  },
438
900
  };
439
901
  }
440
902
 
441
- export { KitTransactionExecutionError, createWalletAdapter, fromAccountRole, fromKitInstruction, toAccountRole, toKitInstruction };
903
+ export { ESTIMATED_COMPUTE_UNIT_HEADROOM_PERCENT, KitTransactionExecutionError, LEGACY_TRANSACTION_SIZE_LIMIT, LOADED_ACCOUNTS_DATA_PAGE_BYTES, MAX_COMPUTE_UNIT_LIMIT, MAX_LOADED_ACCOUNTS_DATA_SIZE, SOLANA_SIGN_AND_SEND_TRANSACTION, SOLANA_SIGN_TRANSACTION, V1_MAX_ACCOUNTS, V1_MAX_INSTRUCTIONS, V1_MAX_SIGNATURES, V1_TRANSACTION_SIZE_LIMIT, WalletStandardSignerError, createWalletAdapter, createWalletStandardSigner, estimatedComputeUnitLimit, estimatedLoadedAccountsDataSize, fromAccountRole, fromKitInstruction, toAccountRole, toKitInstruction };
442
904
  //# sourceMappingURL=index.js.map