@bounded-sh/server 0.0.67 → 0.0.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @bounded-sh/server
2
2
 
3
- Server SDK for the Bounded API. It provides explicit, isolated Solana-keypair or EVM-private-key clients for backend applications; no signer is loaded from ambient process state.
3
+ Server SDK for the Bounded API. It provides explicit, isolated Solana-keypair clients for backend applications; no signer is loaded from ambient process state.
4
4
 
5
5
  ## Installation
6
6
 
@@ -16,7 +16,7 @@ yarn add @bounded-sh/server
16
16
 
17
17
  ## Features
18
18
 
19
- - Server-side authentication using Solana keypairs or EVM private keys (SIWS/SIWE)
19
+ - Server-side authentication using Solana keypairs (SIWS)
20
20
  - In-memory session management (no cookies or local storage required)
21
21
  - Support for all Tarobase data operations (get, getMany, set, query)
22
22
  - Real-time subscriptions
@@ -86,27 +86,6 @@ const signature = await wallet.signMessage("Hello, world!");
86
86
  // const signature = await wallet.signAndSubmitTransaction(transaction);
87
87
  ```
88
88
 
89
- ### EVM Keypair Client
90
-
91
- EVM clients require a trusted RPC and deployed `BoundedRuntime` address. A write response can select only an explicitly configured chain/runtime pair, and the SDK verifies the returned `setDocuments` calldata against the requested app, paths, actions, and values before signing.
92
-
93
- ```typescript
94
- import { init, createWalletClient } from '@bounded-sh/server';
95
-
96
- await init({ appId: 'your-app-id', chain: 'base' });
97
-
98
- const wallet = await createWalletClient({
99
- evmPrivateKey: process.env.EVM_PRIVATE_KEY!,
100
- chainId: 8453,
101
- rpcUrl: process.env.BASE_RPC_URL!,
102
- runtimeAddress: process.env.BOUNDED_RUNTIME_BASE!,
103
- });
104
-
105
- await wallet.set('todos/123', { text: 'Buy milk', completed: false });
106
- ```
107
-
108
- For deliberate multi-chain use, provide `chains: { [chainId]: { rpcUrl, runtimeAddress } }`. There is no implicit RPC, runtime address, or chain switch.
109
-
110
89
  ### Subscriptions
111
90
 
112
91
  ```typescript
@@ -138,16 +117,7 @@ function getWebhookKeysUrl(): string | undefined;
138
117
  ### Authentication
139
118
 
140
119
  ```typescript
141
- function createWalletClient(options:
142
- | { keypair: string }
143
- | {
144
- evmPrivateKey: string;
145
- chainId: number;
146
- rpcUrl?: string;
147
- runtimeAddress?: string;
148
- chains?: Record<string, { rpcUrl: string; runtimeAddress: string; name?: string }>;
149
- }
150
- ): Promise<WalletClient>;
120
+ function createWalletClient(options: { keypair: string }): Promise<WalletClient>;
151
121
  function getAuthProvider(): Promise<AuthProvider>; // always rejects; no process-global provider
152
122
  function getIdToken(): Promise<string | null>; // always rejects; no process-global session
153
123
  ```
@@ -182,15 +152,13 @@ class SolanaKeypairProvider implements AuthProvider {
182
152
  login(): Promise<User | null>;
183
153
  logout(): Promise<void>;
184
154
  signMessage(message: string): Promise<string>;
185
- runTransaction(evmTransactionData?: EVMTransaction, solTransactionData?: SolTransaction, options?: SetOptions): Promise<TransactionResult>;
186
155
  restoreSession(): Promise<User | null>;
187
156
  getNativeMethods(): Promise<any>;
188
157
  }
189
158
  ```
190
159
 
191
- `runTransaction` requires an explicit provider `rpcUrl` plus `solTransactionData.network`
192
- set to `solana_devnet` or `solana_mainnet`; missing RPCs and unknown/missing
193
- networks fail closed.
160
+ `signAndSubmitTransaction` requires an explicit provider `rpcUrl`; a missing RPC
161
+ fails closed.
194
162
 
195
163
  ## Types
196
164
 
@@ -242,7 +210,7 @@ Please see the main repository for contribution guidelines.
242
210
 
243
211
  This server-side SDK differs from the web SDK in several important ways:
244
212
 
245
- 1. **Authentication Method**: Uses explicit Solana or EVM keys instead of browser wallets
213
+ 1. **Authentication Method**: Uses explicit Solana keys instead of browser wallets
246
214
  2. **Storage Strategy**: Uses in-memory storage instead of localStorage/sessionStorage
247
215
  3. **Environment**: Optimized for Node.js/backend environments rather than browsers
248
216
  4. **Session Management**: Does not rely on browser-specific APIs
@@ -18,5 +18,4 @@ export declare class OffchainAuthProvider implements AuthProvider {
18
18
  * for the full implementation with blockhash handling and feePayer support.
19
19
  */
20
20
  signAndSubmitTransaction(_transaction: Transaction | VersionedTransaction, _feePayer?: any): Promise<string>;
21
- runTransaction(evmTransactionData?: any, solTransactionData?: any, options?: any): Promise<any>;
22
21
  }
@@ -1,6 +1,6 @@
1
1
  import { Keypair, Transaction, VersionedTransaction } from '@solana/web3.js';
2
2
  import * as anchor from '@coral-xyz/anchor';
3
- import type { AuthProvider, EVMTransaction, SolTransaction, TransactionResult, User, SetOptions } from '@bounded-sh/core';
3
+ import type { AuthProvider, User } from '@bounded-sh/core';
4
4
  export declare class SolanaKeypairProvider implements AuthProvider {
5
5
  private readonly rpcUrl;
6
6
  private readonly serverKeypair;
@@ -32,7 +32,6 @@ export declare class SolanaKeypairProvider implements AuthProvider {
32
32
  */
33
33
  private submitAndConfirmTransaction;
34
34
  private getRpcUrl;
35
- runTransaction(_evmTransactionData?: EVMTransaction, _solTransactionData?: SolTransaction, _options?: SetOptions): Promise<TransactionResult>;
36
35
  getNativeMethods(): Promise<{
37
36
  keypair: Keypair;
38
37
  }>;
package/dist/index.d.ts CHANGED
@@ -5,12 +5,10 @@ export type { BoundedDecline, DeclineReason, DeclineBoundary } from '@bounded-sh
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
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';
8
+ export type { SetResult, SetEntry, SetDocuments, MirrorSync, SolanaFence, ChainTag, SolanaChainTag, CommittedSetResult, ConfirmedSetResult, SolanaConfirmedSetResult, SubmittedSetResult, SolanaSubmittedSetResult, SolanaExpiredNotLandedSetResult, SolanaSignedSetResult, TransactionResult, TransactionReconciliation, ReconcileSetResultOptions, WaitForSetResultOptions, FileUploadResult, FileDeleteResult, } from '@bounded-sh/core';
9
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';
10
10
  export { getIdToken } from './utils';
11
11
  export { createWalletClient, WalletClient } from './wallet-client';
12
12
  export type { CreateWalletClientOptions } from './wallet-client';
13
- export { EvmKeypairProvider } from './auth/providers/evm-keypair-provider';
14
- export type { EvmKeypairChainConfig } from './auth/providers/evm-keypair-provider';
15
13
  export { verifyWebhook, clearWebhookKeyCache, clearWebhookReplayCache, WebhookVerificationError, DEFAULT_WEBHOOK_KEYS_URL, InMemoryReplayStore, } from './webhooks';
16
14
  export type { WebhookPayload, WebhookPublicKey, WebhookOperation, VerifyWebhookOptions, WebhookHeaders, WebhookReplayStore, } from './webhooks';
package/dist/index.js CHANGED
@@ -5,8 +5,6 @@ var web3_js = require('@solana/web3.js');
5
5
  var bs58 = require('bs58');
6
6
  var buffer = require('buffer');
7
7
  var nacl = require('tweetnacl');
8
- var viem = require('viem');
9
- var accounts = require('viem/accounts');
10
8
 
11
9
  async function init(newConfig) {
12
10
  await core.init(Object.assign(Object.assign({}, newConfig), { authProvider: null, isServer: true, skipBackendInit: true }));
@@ -53,6 +51,20 @@ async function getIdToken() {
53
51
  throw new Error('getIdToken is not available as a tarobase-server top-level auth operation. Use createWalletClient({ keypair }) and call the returned client methods.');
54
52
  }
55
53
 
54
+ /**
55
+ * DS3-0414: does the transaction already carry a REAL signer signature? A Solana
56
+ * signature is computed over the compiled message, which INCLUDES recentBlockhash, so
57
+ * the blockhash must not be rewritten once any signer (sponsor / multisig / co-signer)
58
+ * has signed - a rewrite silently invalidates their signature. Legacy signatures are
59
+ * `{ signature: Buffer | null }` entries; versioned signatures are 64-byte arrays that
60
+ * stay all-zero placeholders until a signer fills them. Mirrors turnkey-signer-bridge.ts.
61
+ */
62
+ function transactionHasSignature(transaction) {
63
+ if ('serializeMessage' in transaction) {
64
+ return transaction.signatures.some((s) => s.signature != null);
65
+ }
66
+ return transaction.signatures.some((sig) => sig.some((b) => b !== 0));
67
+ }
56
68
  class SolanaKeypairProvider {
57
69
  constructor(rpcUrl, serverKeypair) {
58
70
  this.rpcUrl = rpcUrl;
@@ -109,25 +121,34 @@ class SolanaKeypairProvider {
109
121
  // 1. Get RPC URL and create connection
110
122
  const rpcUrl = this.getRpcUrl();
111
123
  const connection = new web3_js.Connection(rpcUrl, 'confirmed');
112
- // 2. Get fresh blockhash and set it on the transaction before signing
113
- const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash('confirmed');
114
- // Use duck typing instead of instanceof to handle multiple @solana/web3.js versions
115
- const isLegacyTransaction = 'recentBlockhash' in transaction && !('message' in transaction && 'staticAccountKeys' in transaction.message);
116
- if (isLegacyTransaction) {
117
- const legacyTx = transaction;
118
- legacyTx.recentBlockhash = blockhash;
119
- legacyTx.lastValidBlockHeight = lastValidBlockHeight;
120
- // Set feePayer if not already set
121
- if (!legacyTx.feePayer) {
122
- legacyTx.feePayer = feePayer !== null && feePayer !== void 0 ? feePayer : this.keypair.publicKey;
124
+ // 2. Get fresh blockhash and set it on the transaction before signing.
125
+ // DS3-0414: a Solana signature covers the compiled message (recentBlockhash included),
126
+ // so refreshing the blockhash after a sponsor / co-signer has already signed silently
127
+ // invalidates their signature - and this is the sponsored/fee-payer submit path, where a
128
+ // co-signed transaction is exactly what arrives. signTransaction only ADDS this keypair's
129
+ // signature (partialSign), it never re-signs the others. So only refresh the blockhash
130
+ // while the transaction is still unsigned; once a real signature is present, sign and
131
+ // submit the caller's message verbatim.
132
+ if (!transactionHasSignature(transaction)) {
133
+ const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash('confirmed');
134
+ // Use duck typing instead of instanceof to handle multiple @solana/web3.js versions
135
+ const isLegacyTransaction = 'recentBlockhash' in transaction && !('message' in transaction && 'staticAccountKeys' in transaction.message);
136
+ if (isLegacyTransaction) {
137
+ const legacyTx = transaction;
138
+ legacyTx.recentBlockhash = blockhash;
139
+ legacyTx.lastValidBlockHeight = lastValidBlockHeight;
140
+ // Set feePayer if not already set
141
+ if (!legacyTx.feePayer) {
142
+ legacyTx.feePayer = feePayer !== null && feePayer !== void 0 ? feePayer : this.keypair.publicKey;
143
+ }
144
+ }
145
+ else {
146
+ // VersionedTransaction
147
+ const versionedTx = transaction;
148
+ versionedTx.message.recentBlockhash = blockhash;
149
+ // Note: VersionedTransaction feePayer is set in the message at creation time
150
+ // and cannot be modified after creation
123
151
  }
124
- }
125
- else {
126
- // VersionedTransaction
127
- const versionedTx = transaction;
128
- versionedTx.message.recentBlockhash = blockhash;
129
- // Note: VersionedTransaction feePayer is set in the message at creation time
130
- // and cannot be modified after creation
131
152
  }
132
153
  // 3. Sign the transaction
133
154
  const signedTx = await this.signTransaction(transaction);
@@ -189,16 +210,6 @@ class SolanaKeypairProvider {
189
210
  }
190
211
  throw new Error('Solana RPC URL is required for server keypair transaction submission; pass an explicit rpcUrl when creating SolanaKeypairProvider.');
191
212
  }
192
- /* ----------------------------------------------------------- *
193
- * Transaction runner
194
- * ----------------------------------------------------------- */
195
- async runTransaction(_evmTransactionData, _solTransactionData, _options) {
196
- // Client-side Solana transaction building is retired: the realtime worker
197
- // returns a fully-built (and, when sponsored, partially-signed) transaction
198
- // on its 202, which the SDK validates and signs via signTransaction /
199
- // signAndSubmitTransaction. This provider serves no EVM payload either.
200
- throw new Error('runTransaction is not supported by this provider; Bounded returns pre-built Solana transactions that are signed with signTransaction/signAndSubmitTransaction.');
201
- }
202
213
  async getNativeMethods() {
203
214
  return { keypair: this.keypair };
204
215
  }
@@ -237,312 +248,6 @@ class OffchainAuthProvider {
237
248
  async signAndSubmitTransaction(_transaction, _feePayer) {
238
249
  throw new Error('Poofnet does not support real Solana transactions. Deploy your project to mainnet to use this feature.');
239
250
  }
240
- async runTransaction(evmTransactionData, solTransactionData, options) {
241
- return this.wrappedProvider.runTransaction(evmTransactionData, solTransactionData, options);
242
- }
243
- }
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
- }
287
- const NO_SOLANA = 'An EVM keypair client cannot sign Solana transactions.';
288
- function normalizeChainConfig(input) {
289
- var _a;
290
- if (!Number.isSafeInteger(input.chainId) || input.chainId <= 0)
291
- throw new Error('EVM chainId must be a positive safe integer');
292
- let rpc;
293
- try {
294
- rpc = new URL(input.rpcUrl);
295
- }
296
- catch (_b) {
297
- throw new Error(`EVM RPC URL is invalid for chain ${input.chainId}`);
298
- }
299
- if (rpc.protocol !== 'https:' && !(rpc.protocol === 'http:' && (rpc.hostname === 'localhost' || rpc.hostname === '127.0.0.1' || rpc.hostname === '[::1]'))) {
300
- throw new Error(`EVM RPC URL must use HTTPS for chain ${input.chainId}`);
301
- }
302
- const runtimeAddress = core.normalizeEvmAddress(input.runtimeAddress);
303
- if (!runtimeAddress)
304
- throw new Error(`BoundedRuntime address is invalid for chain ${input.chainId}`);
305
- return { chainId: input.chainId, rpcUrl: rpc.toString(), runtimeAddress, name: ((_a = input.name) === null || _a === void 0 ? void 0 : _a.trim()) || `EVM ${input.chainId}` };
306
- }
307
- function asViemChain(config) {
308
- return viem.defineChain({
309
- id: config.chainId,
310
- name: config.name,
311
- nativeCurrency: { name: 'Native token', symbol: 'NATIVE', decimals: 18 },
312
- rpcUrls: { default: { http: [config.rpcUrl] } },
313
- });
314
- }
315
- class EvmKeypairProvider {
316
- constructor(privateKey, defaultChainId, chainConfigs) {
317
- this.chains = new Map();
318
- if (!/^0x[0-9a-fA-F]{64}$/.test(privateKey))
319
- throw new Error('Invalid EVM private key: expected 0x followed by 64 hex characters.');
320
- this.account = accounts.privateKeyToAccount(privateKey);
321
- const address = core.normalizeEvmAddress(this.account.address);
322
- if (!address)
323
- throw new Error('Could not derive a valid EVM address from the private key');
324
- this.address = address;
325
- for (const raw of chainConfigs) {
326
- const chain = normalizeChainConfig(raw);
327
- if (this.chains.has(chain.chainId))
328
- throw new Error(`Duplicate EVM chain configuration: ${chain.chainId}`);
329
- this.chains.set(chain.chainId, chain);
330
- }
331
- if (!this.chains.has(defaultChainId))
332
- throw new Error(`Default EVM chain ${defaultChainId} is not configured`);
333
- this.defaultChainId = defaultChainId;
334
- }
335
- user() {
336
- return { id: this.address, address: null, evmAddress: this.address, provider: this };
337
- }
338
- async createBoundedSession(sessionConfig) {
339
- var _a, _b, _c, _d;
340
- // Bind the SIWE nonce/message/session to the snapshotted app+issuer when a
341
- // wallet client supplies one, so a later global init() cannot retarget this
342
- // session mint (CTA-05); otherwise read the mutable global init() config.
343
- const nonce = await core.genAuthNonce(sessionConfig);
344
- const appId = (_a = sessionConfig === null || sessionConfig === void 0 ? void 0 : sessionConfig.appId) !== null && _a !== void 0 ? _a : (await core.getConfig()).appId;
345
- const message = core.genSiweMessage(this.address, nonce, appId, 'server', 'https://server/login', { chainId: this.defaultChainId });
346
- const signature = await this.account.signMessage({ message });
347
- const session = await core.createSessionWithSignature(this.address, message, signature, 'siwe', sessionConfig);
348
- return {
349
- address: this.address,
350
- accessToken: String((_b = session.accessToken) !== null && _b !== void 0 ? _b : ''),
351
- idToken: String((_c = session.idToken) !== null && _c !== void 0 ? _c : ''),
352
- refreshToken: String((_d = session.refreshToken) !== null && _d !== void 0 ? _d : ''),
353
- };
354
- }
355
- async login() { return this.user(); }
356
- async restoreSession() { return this.user(); }
357
- async logout() { }
358
- async signMessage(message) {
359
- return this.account.signMessage({ message });
360
- }
361
- async signTransaction(_transaction) {
362
- throw new Error(NO_SOLANA);
363
- }
364
- async signAndSubmitTransaction(_transaction, _feePayer) {
365
- throw new Error(NO_SOLANA);
366
- }
367
- async runTransaction(evm, sol, options) {
368
- var _a;
369
- if (evm && sol)
370
- throw new Error('A transaction cannot target EVM and Solana at the same time.');
371
- if (!evm || !evm.chainId || !evm.to || !evm.data)
372
- throw new Error('A complete EVM transaction is required');
373
- if ((options === null || options === void 0 ? void 0 : options.shouldSubmitTx) === false)
374
- throw new Error('EVM keypair writes submit atomically; shouldSubmitTx:false is unsupported.');
375
- const config = this.chains.get(evm.chainId);
376
- if (!config)
377
- throw new Error(`EVM chain ${evm.chainId} is not configured for this wallet client`);
378
- if (core.normalizeEvmAddress(evm.to) !== config.runtimeAddress)
379
- throw new Error('EVM transaction target is not the configured BoundedRuntime');
380
- if (evm.nonce !== undefined)
381
- throw new Error('Server-supplied EVM nonces are not accepted');
382
- try {
383
- if (BigInt((_a = evm.value) !== null && _a !== void 0 ? _a : '0') !== BigInt(0))
384
- throw new Error('BoundedRuntime.setDocuments cannot transfer native value');
385
- }
386
- catch (error) {
387
- if (error instanceof Error && error.message.includes('cannot transfer'))
388
- throw error;
389
- throw new Error('Invalid EVM transaction value');
390
- }
391
- // The shared decoder accepts only canonical ordinary/attested document
392
- // writes and rejects selector-prefix smuggling, aliases and trailing bytes.
393
- core.assertCanonicalBoundedRuntimeWriteCalldata(evm.data);
394
- // A server key can safely switch between explicitly configured chains: each
395
- // tx selects its own immutable viem client/RPC/runtime tuple. No untrusted
396
- // server response may introduce a new chain or RPC endpoint.
397
- const chain = asViemChain(config);
398
- const transport = viem.http(config.rpcUrl);
399
- const wallet = viem.createWalletClient({ account: this.account, chain, transport });
400
- const publicClient = viem.createPublicClient({ chain, transport });
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
- }
467
- if (receipt.status !== 'success')
468
- throw new Error(`EVM transaction ${hash} reverted on-chain`);
469
- if (receipt.blockNumber > BigInt(Number.MAX_SAFE_INTEGER))
470
- throw new Error('EVM receipt block number exceeds the SDK safe-integer range');
471
- return {
472
- outcome: 'confirmed',
473
- lane: 'evm',
474
- transactionSignature: hash,
475
- blockNumber: Number(receipt.blockNumber),
476
- gasUsed: receipt.gasUsed.toString(),
477
- data: receipt,
478
- };
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
- }
540
- async getNativeMethods() { return this.account; }
541
- transactionPolicy() {
542
- return {
543
- runtimeAddresses: Object.fromEntries([...this.chains].map(([id, config]) => [String(id), config.runtimeAddress])),
544
- };
545
- }
546
251
  }
547
252
 
548
253
  // #023: strip caller-supplied auth headers so an operation runs as the isolated wallet, not
@@ -602,12 +307,11 @@ function jwtExpiringSoon(token) {
602
307
  /* WalletClient */
603
308
  /* ------------------------------------------------------------------ */
604
309
  class WalletClient {
605
- constructor(provider, sessionManager, address, config, evmTransactionPolicy) {
310
+ constructor(provider, sessionManager, address, config) {
606
311
  this.provider = provider;
607
312
  this.sessionManager = sessionManager;
608
313
  this.address = address;
609
314
  this.config = config;
610
- this.evmTransactionPolicy = evmTransactionPolicy;
611
315
  this.live = {
612
316
  intent: (roomPath, intent, opts = {}) => core.live.intent(roomPath, intent, this.mergeLiveIntentOptions(opts)),
613
317
  // Route status through the SAME auth merge intent uses so a server-keypair
@@ -640,10 +344,12 @@ class WalletClient {
640
344
  }
641
345
  /* ---- Auth override helpers ---- */
642
346
  buildOverrides() {
643
- return Object.assign({ authProvider: this.provider,
347
+ return {
348
+ authProvider: this.provider,
644
349
  // Pin every request to this wallet client's snapshotted app + endpoints so
645
350
  // a later global init() can never retarget its reads/writes (CTA-05).
646
- _config: this.config, _getAuthHeaders: async () => {
351
+ _config: this.config,
352
+ _getAuthHeaders: async () => {
647
353
  let s = await this.sessionManager.getSession();
648
354
  // Keypair-backed session: if the cached token is expired/near-expiry,
649
355
  // re-sign for a fresh one (we always hold the key). Keeps long-lived WS
@@ -654,9 +360,12 @@ class WalletClient {
654
360
  s = await this.sessionManager.getSession();
655
361
  }
656
362
  return { Authorization: `Bearer ${s.idToken}` };
657
- }, _clearAuth: async () => {
363
+ },
364
+ _clearAuth: async () => {
658
365
  this.sessionManager.clearSession();
659
- }, _walletAddress: this.address }, (this.evmTransactionPolicy ? { _evmTransactionPolicy: this.evmTransactionPolicy } : {}));
366
+ },
367
+ _walletAddress: this.address,
368
+ };
660
369
  }
661
370
  /** #023: strip caller Authorization (any casing) so operations run as the isolated wallet. */
662
371
  sanitizeHeaders(headers) {
@@ -665,7 +374,7 @@ class WalletClient {
665
374
  mergeSetOverrides(opts) {
666
375
  var _a;
667
376
  const ovr = this.buildOverrides();
668
- return Object.assign(Object.assign({}, 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, _evmTransactionPolicy: ovr._evmTransactionPolicy, _config: ovr._config }) });
377
+ return Object.assign(Object.assign({}, 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, _config: ovr._config }) });
669
378
  }
670
379
  mergeReadOverrides(existing) {
671
380
  const ovr = this.buildOverrides();
@@ -784,20 +493,13 @@ class WalletClient {
784
493
  }
785
494
  }
786
495
  /**
787
- * Create a self-contained wallet client bound to one Solana or EVM keypair.
496
+ * Create a self-contained wallet client bound to one Solana keypair.
788
497
  * Each client has its own auth session — no global state is mutated.
789
498
  *
790
499
  * @example
791
500
  * ```ts
792
501
  * const vault = await createWalletClient({ keypair: process.env.VAULT_KEY });
793
502
  * await vault.set('markets/123', data);
794
- *
795
- * const evmVault = await createWalletClient({
796
- * evmPrivateKey: process.env.EVM_PRIVATE_KEY,
797
- * chainId: 8453,
798
- * rpcUrl: process.env.BASE_RPC_URL,
799
- * runtimeAddress: process.env.BOUNDED_RUNTIME,
800
- * });
801
503
  * ```
802
504
  */
803
505
  async function createWalletClient(opts) {
@@ -813,32 +515,8 @@ async function createWalletClient(opts) {
813
515
  authApiUrl: config.authApiUrl,
814
516
  authMethod: config.authMethod,
815
517
  };
816
- if ('evmPrivateKey' in opts && typeof opts.evmPrivateKey === 'string') {
817
- const chainConfigs = [];
818
- if (opts.chains) {
819
- for (const [rawId, chain] of Object.entries(opts.chains)) {
820
- const chainId = Number(rawId);
821
- if (!Number.isSafeInteger(chainId) || chainId <= 0)
822
- throw new Error(`Invalid EVM chain id in chains map: ${rawId}`);
823
- chainConfigs.push({ chainId, rpcUrl: chain.rpcUrl, runtimeAddress: chain.runtimeAddress, name: chain.name });
824
- }
825
- }
826
- if (opts.rpcUrl !== undefined || opts.runtimeAddress !== undefined) {
827
- if (!opts.rpcUrl || !opts.runtimeAddress)
828
- throw new Error('Single-chain EVM clients require both rpcUrl and runtimeAddress');
829
- if (chainConfigs.some((chain) => chain.chainId === opts.chainId))
830
- throw new Error(`EVM chain ${opts.chainId} is configured twice`);
831
- chainConfigs.push({ chainId: opts.chainId, rpcUrl: opts.rpcUrl, runtimeAddress: opts.runtimeAddress });
832
- }
833
- if (chainConfigs.length === 0)
834
- throw new Error('EVM wallet clients require an explicit RPC URL and BoundedRuntime address');
835
- const provider = new EvmKeypairProvider(opts.evmPrivateKey, opts.chainId, chainConfigs);
836
- const sessionManager = core.ServerSessionManager.forSessionFactory(() => provider.createBoundedSession(sessionConfig));
837
- await sessionManager.getSession();
838
- return new WalletClient(provider, sessionManager, provider.address, config, provider.transactionPolicy());
839
- }
840
518
  if (!('keypair' in opts) || typeof opts.keypair !== 'string') {
841
- throw new Error('createWalletClient requires exactly one of keypair or evmPrivateKey');
519
+ throw new Error('createWalletClient requires a Solana keypair');
842
520
  }
843
521
  const kp = parseKeypair(opts.keypair);
844
522
  let provider = new SolanaKeypairProvider((_a = config.rpcUrl) !== null && _a !== void 0 ? _a : null, kp);
@@ -1217,7 +895,6 @@ Object.defineProperty(exports, "waitForSetResult", {
1217
895
  get: function () { return core.waitForSetResult; }
1218
896
  });
1219
897
  exports.DEFAULT_WEBHOOK_KEYS_URL = DEFAULT_WEBHOOK_KEYS_URL;
1220
- exports.EvmKeypairProvider = EvmKeypairProvider;
1221
898
  exports.InMemoryReplayStore = InMemoryReplayStore;
1222
899
  exports.WalletClient = WalletClient;
1223
900
  exports.WebhookVerificationError = WebhookVerificationError;