@bounded-sh/server 0.0.65 → 0.0.66

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.
@@ -1,4 +1,4 @@
1
- import type { AuthProvider, EVMTransaction, SolTransaction, TransactionResult, User, SetOptions } from '@bounded-sh/core';
1
+ import type { AuthProvider, EVMTransaction, EvmReconcileRequest, SolTransaction, TransactionReconciliation, TransactionResult, User, SetOptions } from '@bounded-sh/core';
2
2
  import { type SessionMintConfig } from '@bounded-sh/core';
3
3
  import type { PublicKey, Transaction, VersionedTransaction } from '@solana/web3.js';
4
4
  export interface EvmKeypairChainConfig {
@@ -27,6 +27,17 @@ export declare class EvmKeypairProvider implements AuthProvider {
27
27
  signTransaction(_transaction: Transaction | VersionedTransaction): Promise<Transaction | VersionedTransaction>;
28
28
  signAndSubmitTransaction(_transaction: Transaction | VersionedTransaction, _feePayer?: PublicKey): Promise<string>;
29
29
  runTransaction(evm?: EVMTransaction, sol?: SolTransaction, options?: SetOptions): Promise<TransactionResult>;
30
+ /**
31
+ * Poll the SAME hash, and — only while the fenced nonce is still unused — put
32
+ * the byte-identical signed transaction back on the wire. Same nonce, same
33
+ * signature, so a rebroadcast can never produce a second execution.
34
+ *
35
+ * It NEVER reports a retry-safe terminal state. Nonce advancement is not proof
36
+ * the write did not execute: a replacement can occupy the same sender and
37
+ * nonce carrying identical Bounded calldata under a different hash, and plain
38
+ * JSON-RPC offers no transaction-by-sender-and-nonce lookup to identify it.
39
+ */
40
+ reconcileEvmTransaction(request: EvmReconcileRequest): Promise<TransactionReconciliation>;
30
41
  getNativeMethods(): Promise<any>;
31
42
  transactionPolicy(): {
32
43
  runtimeAddresses: Record<string, string>;
package/dist/index.d.ts CHANGED
@@ -4,6 +4,8 @@ export { BoundedDeclineError, deriveDecline, DECLINE_EVENT_NAME } from '@bounded
4
4
  export type { BoundedDecline, DeclineReason, DeclineBoundary } from '@bounded-sh/core';
5
5
  export { getAuthProvider } from './auth';
6
6
  export { get, getMany, set, setMany, setFile, getFiles, search, queryAggregate, runQuery, runQueryMany, runExpression, runExpressionMany, signMessage, signTransaction, signAndSubmitTransaction, count, aggregate, subscribe, functions, invokeFunction, live, liveIntent, liveStatus, } from './explicit-client-only';
7
+ export { reconcileSetResult, waitForSetResult } from '@bounded-sh/core';
8
+ export type { SetResult, SetEntry, SetDocuments, MirrorSync, SolanaFence, EvmFence, ChainTag, SolanaChainTag, EvmChainTag, CommittedSetResult, ConfirmedSetResult, SolanaConfirmedSetResult, EvmConfirmedSetResult, SubmittedSetResult, SolanaSubmittedSetResult, EvmFencedSubmittedSetResult, EvmHashKnownSubmittedSetResult, EvmIrreconcilableSubmittedSetResult, SolanaExpiredNotLandedSetResult, SolanaSignedSetResult, TransactionResult, TransactionReconciliation, EvmReconcileRequest, ReconcileSetResultOptions, WaitForSetResultOptions, FileUploadResult, FileDeleteResult, } from '@bounded-sh/core';
7
9
  export type { AuthProvider, ClientConfig, User, GetOptions, SearchOptions, SetOptions, CountOptions, AggregateOptions, AggregateOperation, AggregateResult, AggregateSpec, AggregateRow, QueryAggregateOptions, GetManyResult, RunExpressionOptions, RunExpressionResult, RequestOverrides, RunQueryOptions, InvokeOptions, LiveIntentOptions, LiveStatus, LiveStatusOptions, } from '@bounded-sh/core';
8
10
  export { getIdToken } from './utils';
9
11
  export { createWalletClient, WalletClient } from './wallet-client';
package/dist/index.js CHANGED
@@ -242,6 +242,48 @@ class OffchainAuthProvider {
242
242
  }
243
243
  }
244
244
 
245
+ function evmErrorText(error) {
246
+ const message = error === null || error === void 0 ? void 0 : error.message;
247
+ return typeof message === 'string' && message.length > 0 ? message : String(error);
248
+ }
249
+ /**
250
+ * Whether a broadcast failure PROVES the transaction never entered the network.
251
+ * A node-level validation rejection is definitive and keeps throwing; a
252
+ * transport failure (timeout, socket reset, aborted request) is ambiguous — the
253
+ * node may hold the bytes — and must resolve as `submitted`.
254
+ *
255
+ * "already known"/"already imported" is deliberately NOT definitive: it means
256
+ * the node already has these exact bytes, which is success, not failure.
257
+ */
258
+ const DEFINITIVE_EVM_SEND_MARKERS = [
259
+ 'nonce too low',
260
+ 'insufficient funds',
261
+ 'intrinsic gas too low',
262
+ 'gas required exceeds',
263
+ 'exceeds block gas limit',
264
+ 'invalid sender',
265
+ 'invalid signature',
266
+ 'execution reverted',
267
+ 'transaction underpriced',
268
+ 'oversized data',
269
+ ];
270
+ function isDefinitiveEvmSendFailure(error) {
271
+ var _a, _b;
272
+ const message = String((_b = (_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : error) !== null && _b !== void 0 ? _b : '').toLowerCase();
273
+ if (!message)
274
+ return false;
275
+ if (message.includes('already known') || message.includes('already imported'))
276
+ return false;
277
+ return DEFINITIVE_EVM_SEND_MARKERS.some(marker => message.includes(marker));
278
+ }
279
+ /** viem raises a dedicated not-found error when a receipt simply is not mined yet. */
280
+ function isReceiptNotFound(error) {
281
+ var _a, _b;
282
+ const name = String((_a = error === null || error === void 0 ? void 0 : error.name) !== null && _a !== void 0 ? _a : '');
283
+ if (name === 'TransactionReceiptNotFoundError')
284
+ return true;
285
+ return String((_b = error === null || error === void 0 ? void 0 : error.message) !== null && _b !== void 0 ? _b : '').toLowerCase().includes('could not be found');
286
+ }
245
287
  const NO_SOLANA = 'An EVM keypair client cannot sign Solana transactions.';
246
288
  function normalizeChainConfig(input) {
247
289
  var _a;
@@ -356,25 +398,145 @@ class EvmKeypairProvider {
356
398
  const transport = viem.http(config.rpcUrl);
357
399
  const wallet = viem.createWalletClient({ account: this.account, chain, transport });
358
400
  const publicClient = viem.createPublicClient({ chain, transport });
359
- const hash = await wallet.sendTransaction({
360
- account: this.account,
361
- chain,
362
- to: config.runtimeAddress,
363
- data: evm.data,
364
- value: BigInt(0),
365
- });
366
- const receipt = await publicClient.waitForTransactionReceipt({ hash });
401
+ // Unlike a browser wallet this provider OWNS the private key, so it prepares
402
+ // and signs locally with an EXPLICIT nonce and derives the hash before the
403
+ // transaction is ever broadcast. Learning the hash only from
404
+ // `sendTransaction()` discards an identifier we could have known, so an
405
+ // accepted-but-lost response would strand a landed write.
406
+ let request;
407
+ let signedTransaction;
408
+ let hash;
409
+ let nonce;
410
+ try {
411
+ request = await wallet.prepareTransactionRequest({
412
+ account: this.account,
413
+ chain,
414
+ to: config.runtimeAddress,
415
+ data: evm.data,
416
+ value: BigInt(0),
417
+ });
418
+ nonce = Number(request.nonce);
419
+ if (!Number.isSafeInteger(nonce) || nonce < 0)
420
+ throw new Error('the prepared transaction carries no usable nonce');
421
+ signedTransaction = await wallet.signTransaction(request);
422
+ hash = viem.keccak256(signedTransaction);
423
+ }
424
+ catch (error) {
425
+ // Preparation and local signing happen entirely before broadcast, so any
426
+ // failure here is definitive.
427
+ throw error;
428
+ }
429
+ const fence = {
430
+ chainId: config.chainId,
431
+ sender: this.address,
432
+ nonce,
433
+ signedTransaction,
434
+ };
435
+ try {
436
+ await publicClient.sendRawTransaction({ serializedTransaction: signedTransaction });
437
+ }
438
+ catch (error) {
439
+ if (isDefinitiveEvmSendFailure(error))
440
+ throw error;
441
+ // Ambiguous transport outcome: the node may already hold these exact
442
+ // bytes. Report the pre-derived hash and the fence so reconciliation can
443
+ // poll it, or rebroadcast the byte-identical bytes while the nonce is
444
+ // unused — which cannot produce a second execution.
445
+ return {
446
+ outcome: 'submitted',
447
+ lane: 'evm',
448
+ transactionSignature: hash,
449
+ fence,
450
+ reason: `Broadcast response was lost or inconclusive: ${evmErrorText(error)}`,
451
+ };
452
+ }
453
+ let receipt;
454
+ try {
455
+ receipt = await publicClient.waitForTransactionReceipt({ hash });
456
+ }
457
+ catch (error) {
458
+ // The bytes were accepted; only the confirmation outcome is unknown.
459
+ return {
460
+ outcome: 'submitted',
461
+ lane: 'evm',
462
+ transactionSignature: hash,
463
+ fence,
464
+ reason: `Confirmation outcome unknown: ${evmErrorText(error)}`,
465
+ };
466
+ }
367
467
  if (receipt.status !== 'success')
368
468
  throw new Error(`EVM transaction ${hash} reverted on-chain`);
369
469
  if (receipt.blockNumber > BigInt(Number.MAX_SAFE_INTEGER))
370
470
  throw new Error('EVM receipt block number exceeds the SDK safe-integer range');
371
471
  return {
472
+ outcome: 'confirmed',
473
+ lane: 'evm',
372
474
  transactionSignature: hash,
373
475
  blockNumber: Number(receipt.blockNumber),
374
476
  gasUsed: receipt.gasUsed.toString(),
375
477
  data: receipt,
376
478
  };
377
479
  }
480
+ /**
481
+ * Poll the SAME hash, and — only while the fenced nonce is still unused — put
482
+ * the byte-identical signed transaction back on the wire. Same nonce, same
483
+ * signature, so a rebroadcast can never produce a second execution.
484
+ *
485
+ * It NEVER reports a retry-safe terminal state. Nonce advancement is not proof
486
+ * the write did not execute: a replacement can occupy the same sender and
487
+ * nonce carrying identical Bounded calldata under a different hash, and plain
488
+ * JSON-RPC offers no transaction-by-sender-and-nonce lookup to identify it.
489
+ */
490
+ async reconcileEvmTransaction(request) {
491
+ var _a, _b;
492
+ const chainId = (_b = (_a = request.fence) === null || _a === void 0 ? void 0 : _a.chainId) !== null && _b !== void 0 ? _b : this.defaultChainId;
493
+ const config = this.chains.get(chainId);
494
+ if (!config)
495
+ return { state: 'unknown', reason: `EVM chain ${chainId} is not configured for this wallet client` };
496
+ const chain = asViemChain(config);
497
+ const transport = viem.http(config.rpcUrl);
498
+ const publicClient = viem.createPublicClient({ chain, transport });
499
+ try {
500
+ const receipt = await publicClient.getTransactionReceipt({ hash: request.transactionHash });
501
+ if (receipt) {
502
+ if (receipt.status !== 'success') {
503
+ return { state: 'failed', reason: `EVM transaction ${request.transactionHash} reverted on-chain` };
504
+ }
505
+ if (receipt.blockNumber > BigInt(Number.MAX_SAFE_INTEGER)) {
506
+ return { state: 'unknown', reason: 'EVM receipt block number exceeds the SDK safe-integer range' };
507
+ }
508
+ return { state: 'confirmed', blockNumber: Number(receipt.blockNumber) };
509
+ }
510
+ }
511
+ catch (error) {
512
+ // viem throws `TransactionReceiptNotFoundError` when it is simply not
513
+ // mined yet; that is not an error condition, just "still unknown".
514
+ if (!isReceiptNotFound(error)) {
515
+ return { state: 'unknown', reason: `receipt lookup failed: ${evmErrorText(error)}` };
516
+ }
517
+ }
518
+ const fence = request.fence;
519
+ if (!request.allowRebroadcast || !fence) {
520
+ return { state: 'unknown', reason: 'no receipt yet for this transaction hash' };
521
+ }
522
+ try {
523
+ const nonceNow = await publicClient.getTransactionCount({ address: fence.sender, blockTag: 'latest' });
524
+ if (nonceNow > fence.nonce) {
525
+ // The nonce is spent. It may have been spent by THIS transaction under a
526
+ // different hash (a replacement), so this is emphatically not retry-safe.
527
+ return {
528
+ state: 'unknown',
529
+ reason: `nonce ${fence.nonce} on ${fence.sender} has been consumed but this hash has no receipt; ` +
530
+ 'the transaction may have been replaced, so the write is not proven absent',
531
+ };
532
+ }
533
+ await publicClient.sendRawTransaction({ serializedTransaction: fence.signedTransaction });
534
+ return { state: 'unknown', reason: 'rebroadcast the byte-identical fenced transaction; still awaiting a receipt' };
535
+ }
536
+ catch (error) {
537
+ return { state: 'unknown', reason: `rebroadcast attempt was inconclusive: ${evmErrorText(error)}` };
538
+ }
539
+ }
378
540
  async getNativeMethods() { return this.account; }
379
541
  transactionPolicy() {
380
542
  return {
@@ -531,6 +693,24 @@ class WalletClient {
531
693
  async setFile(path, file, options) {
532
694
  return core.setFile(path, file, { _overrides: this.mergeReadOverrides(options === null || options === void 0 ? void 0 : options._overrides) });
533
695
  }
696
+ /**
697
+ * Reconcile a `submitted` write receipt through THIS client's own provider
698
+ * and app binding. A bare top-level `reconcileSetResult` cannot serve a wallet
699
+ * client: the provider and config are private to the instance, and a later
700
+ * global `init()` would otherwise reconcile against the wrong app.
701
+ */
702
+ async reconcileSetResult(receipt, options) {
703
+ return core.reconcileSetResult(receipt, this.mergeReconcileOptions(options));
704
+ }
705
+ /** Bounded repeat of {@link reconcileSetResult}; see `waitForSetResult`. */
706
+ async waitForSetResult(receipt, options) {
707
+ return core.waitForSetResult(receipt, this.mergeReconcileOptions(options));
708
+ }
709
+ mergeReconcileOptions(opts) {
710
+ var _a;
711
+ const ovr = this.buildOverrides();
712
+ return Object.assign(Object.assign({}, (opts !== null && opts !== void 0 ? opts : {})), { _overrides: Object.assign(Object.assign({}, opts === null || opts === void 0 ? void 0 : opts._overrides), { headers: this.sanitizeHeaders((_a = opts === null || opts === void 0 ? void 0 : opts._overrides) === null || _a === void 0 ? void 0 : _a.headers), authProvider: ovr.authProvider, _getAuthHeaders: ovr._getAuthHeaders, _clearAuth: ovr._clearAuth, _walletAddress: ovr._walletAddress, _config: ovr._config }) });
713
+ }
534
714
  async get(path, opts) {
535
715
  return core.get(path, Object.assign(Object.assign({}, opts), { _overrides: this.mergeReadOverrides(opts === null || opts === void 0 ? void 0 : opts._overrides) }));
536
716
  }
@@ -1028,6 +1208,14 @@ Object.defineProperty(exports, "getWebhookKeysUrl", {
1028
1208
  enumerable: true,
1029
1209
  get: function () { return core.getWebhookKeysUrl; }
1030
1210
  });
1211
+ Object.defineProperty(exports, "reconcileSetResult", {
1212
+ enumerable: true,
1213
+ get: function () { return core.reconcileSetResult; }
1214
+ });
1215
+ Object.defineProperty(exports, "waitForSetResult", {
1216
+ enumerable: true,
1217
+ get: function () { return core.waitForSetResult; }
1218
+ });
1031
1219
  exports.DEFAULT_WEBHOOK_KEYS_URL = DEFAULT_WEBHOOK_KEYS_URL;
1032
1220
  exports.EvmKeypairProvider = EvmKeypairProvider;
1033
1221
  exports.InMemoryReplayStore = InMemoryReplayStore;