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