@bounded-sh/server 0.0.17 → 0.0.18
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 +9 -4
- package/dist/auth/index.d.ts +0 -3
- package/dist/auth/providers/solana-keypair-provider.d.ts +5 -3
- package/dist/index.js +271 -49
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +273 -51
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -94,14 +94,14 @@ import { Keypair } from '@solana/web3.js';
|
|
|
94
94
|
await init({
|
|
95
95
|
apiKey: 'your-api-key',
|
|
96
96
|
appId: 'your-app-id',
|
|
97
|
-
rpcUrl:
|
|
97
|
+
rpcUrl: process.env.SOLANA_MAINNET_RPC_URL
|
|
98
98
|
});
|
|
99
99
|
|
|
100
100
|
// Get the auth provider (automatically created during init)
|
|
101
101
|
const provider = await getAuthProvider() as SolanaKeypairProvider;
|
|
102
102
|
|
|
103
|
-
// Or create a provider instance directly
|
|
104
|
-
// const provider = new SolanaKeypairProvider("https://
|
|
103
|
+
// Or create a provider instance directly with your RPC endpoint
|
|
104
|
+
// const provider = new SolanaKeypairProvider("https://your-solana-rpc.example");
|
|
105
105
|
|
|
106
106
|
// Set the keypair
|
|
107
107
|
provider.setKeypair(Keypair.generate());
|
|
@@ -115,6 +115,7 @@ const signature = await provider.signMessage("Hello, world!");
|
|
|
115
115
|
// Run a transaction (example)
|
|
116
116
|
const txResult = await provider.runTransaction(undefined, {
|
|
117
117
|
appId: 'your-app-id',
|
|
118
|
+
network: 'solana_mainnet',
|
|
118
119
|
txArgs: [{
|
|
119
120
|
setDocumentData: [{ path: 'todos/123', data: { text: 'New todo' } }],
|
|
120
121
|
deletePaths: [],
|
|
@@ -191,7 +192,7 @@ function unsubscribe(path: string): Promise<void>;
|
|
|
191
192
|
|
|
192
193
|
```typescript
|
|
193
194
|
class SolanaKeypairProvider implements AuthProvider {
|
|
194
|
-
constructor(
|
|
195
|
+
constructor(rpcUrl?: string);
|
|
195
196
|
|
|
196
197
|
setKeypair(keypair: Keypair | string | Uint8Array): void;
|
|
197
198
|
login(): Promise<User | null>;
|
|
@@ -203,6 +204,10 @@ class SolanaKeypairProvider implements AuthProvider {
|
|
|
203
204
|
}
|
|
204
205
|
```
|
|
205
206
|
|
|
207
|
+
`runTransaction` requires an explicit provider `rpcUrl` plus `solTransactionData.network`
|
|
208
|
+
set to `solana_devnet`, `solana_mainnet`, or `surfnet`; missing RPCs and unknown/missing
|
|
209
|
+
networks fail closed.
|
|
210
|
+
|
|
206
211
|
## Types
|
|
207
212
|
|
|
208
213
|
```typescript
|
package/dist/auth/index.d.ts
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
1
|
import { AuthProvider } from '@bounded-sh/core';
|
|
2
|
-
export declare const SOLANA_DEVNET_RPC_URL = "https://idelle-8nxsep-fast-devnet.helius-rpc.com";
|
|
3
|
-
export declare const SOLANA_MAINNET_RPC_URL = "https://celestia-cegncv-fast-mainnet.helius-rpc.com";
|
|
4
|
-
export declare const SURFNET_RPC_URL = "https://surfpool.fly.dev";
|
|
5
2
|
export declare function getAuthProvider(): Promise<AuthProvider>;
|
|
6
3
|
export declare function matchAuthProvider(rpcUrl: string | null): Promise<AuthProvider>;
|
|
@@ -2,10 +2,10 @@ import { Keypair, Transaction, VersionedTransaction } from '@solana/web3.js';
|
|
|
2
2
|
import * as anchor from '@coral-xyz/anchor';
|
|
3
3
|
import type { AuthProvider, EVMTransaction, SolTransaction, TransactionResult, User, SetOptions } from '@bounded-sh/core';
|
|
4
4
|
export declare class SolanaKeypairProvider implements AuthProvider {
|
|
5
|
-
private readonly
|
|
5
|
+
private readonly rpcUrl;
|
|
6
6
|
private explicitKeypair?;
|
|
7
7
|
private cachedKeypair?;
|
|
8
|
-
constructor(
|
|
8
|
+
constructor(rpcUrl?: string | null, keypair?: Keypair);
|
|
9
9
|
private get keypair();
|
|
10
10
|
login(): Promise<User | null>;
|
|
11
11
|
restoreSession(): Promise<User | null>;
|
|
@@ -17,7 +17,8 @@ export declare class SolanaKeypairProvider implements AuthProvider {
|
|
|
17
17
|
*
|
|
18
18
|
* This method handles blockhash and transaction confirmation automatically - you do NOT need to
|
|
19
19
|
* set recentBlockhash or lastValidBlockHeight on the transaction before calling this method.
|
|
20
|
-
*
|
|
20
|
+
* Requires an explicit RPC URL configured on the provider because this method has no
|
|
21
|
+
* Solana transaction payload network to resolve.
|
|
21
22
|
*
|
|
22
23
|
* @param transaction - The transaction to sign and submit (Transaction or VersionedTransaction)
|
|
23
24
|
* @param feePayer - Optional fee payer public key. If not provided and the transaction doesn't
|
|
@@ -31,6 +32,7 @@ export declare class SolanaKeypairProvider implements AuthProvider {
|
|
|
31
32
|
* Shared by both signAndSubmitTransaction and runTransactionInner.
|
|
32
33
|
*/
|
|
33
34
|
private submitAndConfirmTransaction;
|
|
35
|
+
private requireSupportedNetwork;
|
|
34
36
|
private getRpcUrl;
|
|
35
37
|
runTransaction(_evm?: EVMTransaction, sol?: SolTransaction, opts?: SetOptions): Promise<TransactionResult>;
|
|
36
38
|
private runTransactionInner;
|
package/dist/index.js
CHANGED
|
@@ -36,6 +36,25 @@ var crypto__namespace = /*#__PURE__*/_interopNamespaceDefault(crypto);
|
|
|
36
36
|
* is the canonical name (it matches the CLI's `BOUNDED_PRIVATE_KEY`).
|
|
37
37
|
* ------------------------------------------------------------- */
|
|
38
38
|
const ENV_VARS = ['BOUNDED_PRIVATE_KEY']; // base-58 or JSON array
|
|
39
|
+
const PRESIGNED_BLOCKHASH_ERROR = 'Server signedTransaction blockhash is stale or expired';
|
|
40
|
+
const BOUNDED_PROGRAM_MAINNET = 'poof4b5pk1L9tmThvBmaABjcyjfhFGbMbQP5BXk2QZp';
|
|
41
|
+
const BOUNDED_PROGRAM_DEVNET = 'taro6CvKqwrYrDc16ufYgzQ2NZcyyVKStffbtudrhRu';
|
|
42
|
+
const COMPUTE_BUDGET_PROGRAM = 'ComputeBudget111111111111111111111111111111';
|
|
43
|
+
const SYSTEM_PROGRAM_ID = '11111111111111111111111111111111';
|
|
44
|
+
const SUPPORTED_SOLANA_NETWORKS = ['solana_devnet', 'solana_mainnet', 'surfnet'];
|
|
45
|
+
const SUPPORTED_SOLANA_NETWORK_SET = new Set(SUPPORTED_SOLANA_NETWORKS);
|
|
46
|
+
const ALLOWED_SERVER_TX_PROGRAMS = new Set([
|
|
47
|
+
BOUNDED_PROGRAM_MAINNET,
|
|
48
|
+
BOUNDED_PROGRAM_DEVNET,
|
|
49
|
+
COMPUTE_BUDGET_PROGRAM,
|
|
50
|
+
SYSTEM_PROGRAM_ID,
|
|
51
|
+
]);
|
|
52
|
+
const SET_DOCUMENTS_DISCRIMINATOR = '79,46,72,73,24,79,66,245';
|
|
53
|
+
const SET_DOCUMENTS_V2_DISCRIMINATOR = '22,236,242,185,145,61,26,39';
|
|
54
|
+
const ALLOWED_BOUNDED_SET_DISCRIMINATORS = new Set([
|
|
55
|
+
SET_DOCUMENTS_DISCRIMINATOR,
|
|
56
|
+
SET_DOCUMENTS_V2_DISCRIMINATOR,
|
|
57
|
+
]);
|
|
39
58
|
function loadKeypairFromEnv() {
|
|
40
59
|
var _a;
|
|
41
60
|
let secret;
|
|
@@ -84,23 +103,214 @@ async function fetchPriorityFee(rpcEndpoint, rawUnsignedTxBuffer) {
|
|
|
84
103
|
});
|
|
85
104
|
const data = await res.json();
|
|
86
105
|
const fee = (_b = (_a = data === null || data === void 0 ? void 0 : data.result) === null || _a === void 0 ? void 0 : _a.priorityFeeEstimate) !== null && _b !== void 0 ? _b : null;
|
|
87
|
-
console.log("Got fee from Helius", fee);
|
|
88
106
|
if (fee != null && fee > 0) {
|
|
89
107
|
return Math.ceil(Number(fee) * 1.2);
|
|
90
108
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
return Math.ceil(10000 * 1.2);
|
|
94
|
-
}
|
|
109
|
+
console.warn("Priority-fee estimate returned no positive fee; skipping priority fee instruction");
|
|
110
|
+
return null;
|
|
95
111
|
}
|
|
96
112
|
catch (err) {
|
|
97
|
-
console.warn("Priority
|
|
98
|
-
return
|
|
113
|
+
console.warn("Priority-fee estimate failed; skipping priority fee instruction:", err);
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function isPresignedBlockhashError(error) {
|
|
118
|
+
return typeof (error === null || error === void 0 ? void 0 : error.message) === 'string' && error.message.includes(PRESIGNED_BLOCKHASH_ERROR);
|
|
119
|
+
}
|
|
120
|
+
function isPermanentPresignedTransactionError(error) {
|
|
121
|
+
return isPresignedBlockhashError(error) ||
|
|
122
|
+
(typeof (error === null || error === void 0 ? void 0 : error.message) === 'string' && (error.message.startsWith('Server signedTransaction') ||
|
|
123
|
+
error.message.startsWith('Server preInstruction')));
|
|
124
|
+
}
|
|
125
|
+
class BorshCursor {
|
|
126
|
+
constructor(data, offset) {
|
|
127
|
+
this.data = data;
|
|
128
|
+
this.offset = offset;
|
|
129
|
+
}
|
|
130
|
+
requireBytes(length, field) {
|
|
131
|
+
if (this.offset + length > this.data.length) {
|
|
132
|
+
throw new Error(`Server signedTransaction has malformed Bounded instruction data while reading ${field}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
readU8(field) {
|
|
136
|
+
this.requireBytes(1, field);
|
|
137
|
+
return this.data[this.offset++];
|
|
138
|
+
}
|
|
139
|
+
readU32(field) {
|
|
140
|
+
this.requireBytes(4, field);
|
|
141
|
+
const value = this.data[this.offset] |
|
|
142
|
+
(this.data[this.offset + 1] << 8) |
|
|
143
|
+
(this.data[this.offset + 2] << 16) |
|
|
144
|
+
(this.data[this.offset + 3] << 24);
|
|
145
|
+
this.offset += 4;
|
|
146
|
+
return value >>> 0;
|
|
147
|
+
}
|
|
148
|
+
skip(length, field) {
|
|
149
|
+
this.requireBytes(length, field);
|
|
150
|
+
this.offset += length;
|
|
151
|
+
}
|
|
152
|
+
readString(field) {
|
|
153
|
+
const length = this.readU32(`${field} length`);
|
|
154
|
+
this.requireBytes(length, field);
|
|
155
|
+
const raw = this.data.slice(this.offset, this.offset + length);
|
|
156
|
+
this.offset += length;
|
|
157
|
+
return buffer.Buffer.from(raw).toString('utf8');
|
|
158
|
+
}
|
|
159
|
+
skipBytes(field) {
|
|
160
|
+
const length = this.readU32(`${field} length`);
|
|
161
|
+
this.skip(length, field);
|
|
162
|
+
}
|
|
163
|
+
isAtEnd() {
|
|
164
|
+
return this.offset === this.data.length;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function normalizeOnchainPath(path) {
|
|
168
|
+
let normalized = path.startsWith('/') ? path.slice(1) : path;
|
|
169
|
+
if (normalized.endsWith('*') && normalized.length > 1) {
|
|
170
|
+
normalized = normalized.slice(0, -1);
|
|
171
|
+
}
|
|
172
|
+
if (normalized.endsWith('/')) {
|
|
173
|
+
normalized = normalized.slice(0, -1);
|
|
174
|
+
}
|
|
175
|
+
return normalized;
|
|
176
|
+
}
|
|
177
|
+
function skipBoundedFieldValue(cursor) {
|
|
178
|
+
const option = cursor.readU8('operation value option');
|
|
179
|
+
if (option === 0)
|
|
180
|
+
return;
|
|
181
|
+
if (option !== 1) {
|
|
182
|
+
throw new Error('Server signedTransaction has malformed Bounded field value option');
|
|
183
|
+
}
|
|
184
|
+
const variant = cursor.readU8('operation value variant');
|
|
185
|
+
switch (variant) {
|
|
186
|
+
case 0:
|
|
187
|
+
case 1:
|
|
188
|
+
cursor.skip(8, 'operation numeric value');
|
|
189
|
+
return;
|
|
190
|
+
case 2:
|
|
191
|
+
cursor.skip(1, 'operation bool value');
|
|
192
|
+
return;
|
|
193
|
+
case 3:
|
|
194
|
+
cursor.readString('operation string value');
|
|
195
|
+
return;
|
|
196
|
+
case 4:
|
|
197
|
+
cursor.skip(32, 'operation address value');
|
|
198
|
+
return;
|
|
199
|
+
default:
|
|
200
|
+
throw new Error(`Server signedTransaction has unsupported Bounded field value variant: ${variant}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function skipBoundedFieldOperation(cursor) {
|
|
204
|
+
cursor.readString('operation key');
|
|
205
|
+
skipBoundedFieldValue(cursor);
|
|
206
|
+
cursor.skip(1, 'operation kind');
|
|
207
|
+
}
|
|
208
|
+
function skipBoundedTxData(cursor, isV2) {
|
|
209
|
+
cursor.readString('txData plugin function key');
|
|
210
|
+
cursor.skipBytes('txData bytes');
|
|
211
|
+
if (isV2) {
|
|
212
|
+
cursor.skipBytes('txData raIndices');
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const raIndexCount = cursor.readU32('txData raIndices length');
|
|
216
|
+
cursor.skip(raIndexCount * 8, 'txData raIndices');
|
|
217
|
+
}
|
|
218
|
+
function parseBoundedSetDocumentsInstruction(data) {
|
|
219
|
+
if (data.length < 8) {
|
|
220
|
+
throw new Error('Server signedTransaction has malformed Bounded instruction data');
|
|
221
|
+
}
|
|
222
|
+
const discriminator = Array.from(data.slice(0, 8)).join(',');
|
|
223
|
+
if (!ALLOWED_BOUNDED_SET_DISCRIMINATORS.has(discriminator)) {
|
|
224
|
+
throw new Error('Server signedTransaction contains unsupported Bounded instruction');
|
|
225
|
+
}
|
|
226
|
+
const isV2 = discriminator === SET_DOCUMENTS_V2_DISCRIMINATOR;
|
|
227
|
+
const cursor = new BorshCursor(data, 8);
|
|
228
|
+
const appId = cursor.readString('appId');
|
|
229
|
+
const documentPaths = [];
|
|
230
|
+
const documentCount = cursor.readU32('documents length');
|
|
231
|
+
for (let i = 0; i < documentCount; i++) {
|
|
232
|
+
documentPaths.push(normalizeOnchainPath(cursor.readString('document path')));
|
|
233
|
+
const operationCount = cursor.readU32('operations length');
|
|
234
|
+
for (let j = 0; j < operationCount; j++) {
|
|
235
|
+
skipBoundedFieldOperation(cursor);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const deletePaths = [];
|
|
239
|
+
const deleteCount = cursor.readU32('delete paths length');
|
|
240
|
+
for (let i = 0; i < deleteCount; i++) {
|
|
241
|
+
deletePaths.push(normalizeOnchainPath(cursor.readString('delete path')));
|
|
242
|
+
}
|
|
243
|
+
const txDataCount = cursor.readU32('txData length');
|
|
244
|
+
for (let i = 0; i < txDataCount; i++) {
|
|
245
|
+
skipBoundedTxData(cursor, isV2);
|
|
246
|
+
}
|
|
247
|
+
const simulate = cursor.readU8('simulate');
|
|
248
|
+
if (simulate !== 0 && simulate !== 1) {
|
|
249
|
+
throw new Error('Server signedTransaction has malformed Bounded simulate flag');
|
|
250
|
+
}
|
|
251
|
+
if (!cursor.isAtEnd()) {
|
|
252
|
+
throw new Error('Server signedTransaction has trailing Bounded instruction data');
|
|
253
|
+
}
|
|
254
|
+
return { appId, documentPaths, deletePaths };
|
|
255
|
+
}
|
|
256
|
+
function assertSamePathSet(expectedPaths, actualPaths) {
|
|
257
|
+
const expected = new Set(expectedPaths.map(normalizeOnchainPath).filter(Boolean));
|
|
258
|
+
const actual = new Set(actualPaths.map(normalizeOnchainPath).filter(Boolean));
|
|
259
|
+
const missing = [...expected].filter(path => !actual.has(path));
|
|
260
|
+
const extra = [...actual].filter(path => !expected.has(path));
|
|
261
|
+
if (missing.length > 0 || extra.length > 0) {
|
|
262
|
+
const details = [
|
|
263
|
+
missing.length ? `missing paths: ${missing.join(', ')}` : '',
|
|
264
|
+
extra.length ? `unexpected paths: ${extra.join(', ')}` : '',
|
|
265
|
+
].filter(Boolean).join('; ');
|
|
266
|
+
throw new Error(`Server signedTransaction Bounded instruction paths do not match requested write paths (${details})`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function validatePresignedSignedTransaction(transaction, sol) {
|
|
270
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
271
|
+
const accountKeys = transaction.message.staticAccountKeys;
|
|
272
|
+
let boundedInstructionCount = 0;
|
|
273
|
+
const actualWritePaths = [];
|
|
274
|
+
for (const ix of transaction.message.compiledInstructions) {
|
|
275
|
+
if (ix.programIdIndex >= accountKeys.length) {
|
|
276
|
+
throw new Error('Server signedTransaction has program ID in lookup table (not allowed)');
|
|
277
|
+
}
|
|
278
|
+
const programId = accountKeys[ix.programIdIndex].toBase58();
|
|
279
|
+
if (!ALLOWED_SERVER_TX_PROGRAMS.has(programId)) {
|
|
280
|
+
throw new Error(`Server signedTransaction contains unauthorized program: ${programId}`);
|
|
281
|
+
}
|
|
282
|
+
const data = ix.data instanceof Uint8Array ? ix.data : buffer.Buffer.from((_a = ix.data) !== null && _a !== void 0 ? _a : []);
|
|
283
|
+
if (programId === SYSTEM_PROGRAM_ID) {
|
|
284
|
+
throw new Error('Server signedTransaction contains unauthorized System Program instruction');
|
|
285
|
+
}
|
|
286
|
+
if (programId === BOUNDED_PROGRAM_MAINNET || programId === BOUNDED_PROGRAM_DEVNET) {
|
|
287
|
+
boundedInstructionCount += 1;
|
|
288
|
+
const parsed = parseBoundedSetDocumentsInstruction(data);
|
|
289
|
+
if (parsed.appId !== sol.appId) {
|
|
290
|
+
throw new Error('Server signedTransaction Bounded instruction appId does not match configured appId');
|
|
291
|
+
}
|
|
292
|
+
actualWritePaths.push(...parsed.documentPaths, ...parsed.deletePaths);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (boundedInstructionCount !== 1) {
|
|
296
|
+
throw new Error('Server signedTransaction must contain exactly one Bounded set-documents instruction');
|
|
297
|
+
}
|
|
298
|
+
const expectedWritePaths = [
|
|
299
|
+
...(((_d = (_c = (_b = sol.txArgs) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.setDocumentData) !== null && _d !== void 0 ? _d : []).map((doc) => doc.path)),
|
|
300
|
+
...((_g = (_f = (_e = sol.txArgs) === null || _e === void 0 ? void 0 : _e[0]) === null || _f === void 0 ? void 0 : _f.deletePaths) !== null && _g !== void 0 ? _g : []),
|
|
301
|
+
];
|
|
302
|
+
assertSamePathSet(expectedWritePaths, actualWritePaths);
|
|
303
|
+
}
|
|
304
|
+
function assertAllowedServerPreInstructions(preInstructions) {
|
|
305
|
+
for (const [index, ix] of (preInstructions !== null && preInstructions !== void 0 ? preInstructions : []).entries()) {
|
|
306
|
+
if (ix.programId.equals(web3_js.SystemProgram.programId)) {
|
|
307
|
+
throw new Error(`Server preInstruction[${index}] contains unauthorized System Program instruction`);
|
|
308
|
+
}
|
|
99
309
|
}
|
|
100
310
|
}
|
|
101
311
|
class SolanaKeypairProvider {
|
|
102
|
-
constructor(
|
|
103
|
-
this.
|
|
312
|
+
constructor(rpcUrl = null, keypair) {
|
|
313
|
+
this.rpcUrl = rpcUrl;
|
|
104
314
|
this.explicitKeypair = keypair;
|
|
105
315
|
}
|
|
106
316
|
get keypair() {
|
|
@@ -139,7 +349,8 @@ class SolanaKeypairProvider {
|
|
|
139
349
|
*
|
|
140
350
|
* This method handles blockhash and transaction confirmation automatically - you do NOT need to
|
|
141
351
|
* set recentBlockhash or lastValidBlockHeight on the transaction before calling this method.
|
|
142
|
-
*
|
|
352
|
+
* Requires an explicit RPC URL configured on the provider because this method has no
|
|
353
|
+
* Solana transaction payload network to resolve.
|
|
143
354
|
*
|
|
144
355
|
* @param transaction - The transaction to sign and submit (Transaction or VersionedTransaction)
|
|
145
356
|
* @param feePayer - Optional fee payer public key. If not provided and the transaction doesn't
|
|
@@ -223,20 +434,23 @@ class SolanaKeypairProvider {
|
|
|
223
434
|
}
|
|
224
435
|
return { signature: sig, txInfo };
|
|
225
436
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (network === 'solana_devnet') {
|
|
231
|
-
return SOLANA_DEVNET_RPC_URL;
|
|
437
|
+
requireSupportedNetwork(network) {
|
|
438
|
+
const networkName = network === null || network === void 0 ? void 0 : network.trim();
|
|
439
|
+
if (!networkName) {
|
|
440
|
+
throw new Error(`Supported sol.network is required for Solana transaction submission; expected one of: ${SUPPORTED_SOLANA_NETWORKS.join(', ')}.`);
|
|
232
441
|
}
|
|
233
|
-
|
|
234
|
-
|
|
442
|
+
if (!SUPPORTED_SOLANA_NETWORK_SET.has(networkName)) {
|
|
443
|
+
throw new Error(`Unsupported Solana network "${networkName}". Supported networks: ${SUPPORTED_SOLANA_NETWORKS.join(', ')}.`);
|
|
235
444
|
}
|
|
236
|
-
|
|
237
|
-
|
|
445
|
+
return networkName;
|
|
446
|
+
}
|
|
447
|
+
getRpcUrl() {
|
|
448
|
+
var _a;
|
|
449
|
+
const explicitRpcUrl = (_a = this.rpcUrl) === null || _a === void 0 ? void 0 : _a.trim();
|
|
450
|
+
if (explicitRpcUrl) {
|
|
451
|
+
return explicitRpcUrl;
|
|
238
452
|
}
|
|
239
|
-
|
|
453
|
+
throw new Error('Solana RPC URL is required for server keypair transaction submission; pass an explicit rpcUrl when creating SolanaKeypairProvider.');
|
|
240
454
|
}
|
|
241
455
|
/* ----------------------------------------------------------- *
|
|
242
456
|
* Transaction runner
|
|
@@ -245,6 +459,8 @@ class SolanaKeypairProvider {
|
|
|
245
459
|
if (!sol)
|
|
246
460
|
throw new Error('Solana transaction data required');
|
|
247
461
|
const kp = this.keypair;
|
|
462
|
+
this.requireSupportedNetwork(sol.network);
|
|
463
|
+
const rpcUrl = this.getRpcUrl();
|
|
248
464
|
// Helper for duck typing - checks if transaction is legacy Transaction vs VersionedTransaction
|
|
249
465
|
const isLegacyTx = (tx) => {
|
|
250
466
|
return 'recentBlockhash' in tx && !('message' in tx && 'staticAccountKeys' in tx.message);
|
|
@@ -293,11 +509,14 @@ class SolanaKeypairProvider {
|
|
|
293
509
|
let errorMessage = "";
|
|
294
510
|
while (retries < 5) {
|
|
295
511
|
try {
|
|
296
|
-
toReturn = await this.runTransactionInner(sol, opts, wallet, kp, deduped);
|
|
512
|
+
toReturn = await this.runTransactionInner(sol, opts, wallet, kp, deduped, rpcUrl);
|
|
297
513
|
didPass = true;
|
|
298
514
|
break;
|
|
299
515
|
}
|
|
300
516
|
catch (error) {
|
|
517
|
+
if (isPermanentPresignedTransactionError(error)) {
|
|
518
|
+
throw error;
|
|
519
|
+
}
|
|
301
520
|
console.log("Error building and sending transaction on retry:", retries, error);
|
|
302
521
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
303
522
|
// Exponential backoff
|
|
@@ -311,9 +530,8 @@ class SolanaKeypairProvider {
|
|
|
311
530
|
}
|
|
312
531
|
return toReturn;
|
|
313
532
|
}
|
|
314
|
-
async runTransactionInner(sol, opts, wallet, kp, deduped) {
|
|
533
|
+
async runTransactionInner(sol, opts, wallet, kp, deduped, rpcUrl) {
|
|
315
534
|
var _a, _b, _c;
|
|
316
|
-
let rpcUrl = this.getRpcUrl(sol.network);
|
|
317
535
|
const connection = new web3_js.Connection(rpcUrl, 'confirmed');
|
|
318
536
|
const anchorProvider = new anchor__namespace.AnchorProvider(connection, wallet, anchor__namespace.AnchorProvider.defaultOptions());
|
|
319
537
|
const app_id = sol.appId;
|
|
@@ -327,11 +545,16 @@ class SolanaKeypairProvider {
|
|
|
327
545
|
let lastValidBlockHeight;
|
|
328
546
|
if (sol.signedTransaction) {
|
|
329
547
|
tx = web3_js.VersionedTransaction.deserialize(buffer.Buffer.from(sol.signedTransaction, 'base64'));
|
|
330
|
-
|
|
331
|
-
blockhash =
|
|
332
|
-
|
|
548
|
+
validatePresignedSignedTransaction(tx, sol);
|
|
549
|
+
blockhash = tx.message.recentBlockhash;
|
|
550
|
+
const isValid = await connection.isBlockhashValid(blockhash, { commitment: 'confirmed' });
|
|
551
|
+
if (!isValid.value) {
|
|
552
|
+
throw new Error(`${PRESIGNED_BLOCKHASH_ERROR}; request a fresh server-built transaction.`);
|
|
553
|
+
}
|
|
554
|
+
lastValidBlockHeight = 0;
|
|
333
555
|
}
|
|
334
556
|
else {
|
|
557
|
+
assertAllowedServerPreInstructions(sol.preInstructions);
|
|
335
558
|
const result = await core.buildSetDocumentsTransaction(connection, sol.txArgs[0].idl, anchorProvider, kp.publicKey, {
|
|
336
559
|
app_id,
|
|
337
560
|
documents: sol.txArgs[0].setDocumentData,
|
|
@@ -346,31 +569,33 @@ class SolanaKeypairProvider {
|
|
|
346
569
|
const isLegacyTx = (t) => {
|
|
347
570
|
return 'recentBlockhash' in t && !('message' in t && 'staticAccountKeys' in t.message);
|
|
348
571
|
};
|
|
349
|
-
// Refresh blockhash and lastValidBlockHeight
|
|
350
572
|
// Use duck typing instead of instanceof to handle multiple @solana/web3.js versions
|
|
351
573
|
if (isLegacyTx(tx)) {
|
|
574
|
+
if (!sol.signedTransaction) {
|
|
575
|
+
tx.recentBlockhash = blockhash;
|
|
576
|
+
tx.lastValidBlockHeight = lastValidBlockHeight;
|
|
577
|
+
tx.feePayer = kp.publicKey;
|
|
578
|
+
}
|
|
352
579
|
tx.partialSign(kp);
|
|
353
|
-
tx.recentBlockhash = blockhash;
|
|
354
|
-
tx.lastValidBlockHeight = lastValidBlockHeight;
|
|
355
|
-
tx.feePayer = kp.publicKey;
|
|
356
580
|
}
|
|
357
581
|
else {
|
|
582
|
+
if (!sol.signedTransaction) {
|
|
583
|
+
tx.message.recentBlockhash = blockhash;
|
|
584
|
+
}
|
|
358
585
|
tx.sign([kp]);
|
|
359
|
-
tx.message.recentBlockhash = blockhash;
|
|
360
586
|
}
|
|
361
|
-
// 3️⃣ Optional priority
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
if (
|
|
367
|
-
|
|
368
|
-
tx
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
// Rebuild the transaction with the new priority fee
|
|
587
|
+
// 3️⃣ Optional priority-fee instruction. Pre-signed transactions are
|
|
588
|
+
// immutable at this point; preserve their exact message and skip estimation.
|
|
589
|
+
if (!sol.signedTransaction) {
|
|
590
|
+
const rawUnsigned = tx.serialize({ requireAllSignatures: false });
|
|
591
|
+
const microLamports = await fetchPriorityFee(connection.rpcEndpoint, rawUnsigned);
|
|
592
|
+
if (microLamports != null) {
|
|
593
|
+
// Use duck typing instead of instanceof to handle multiple @solana/web3.js versions
|
|
594
|
+
if (isLegacyTx(tx)) {
|
|
595
|
+
tx.instructions.unshift(web3_js.ComputeBudgetProgram.setComputeUnitPrice({ microLamports }));
|
|
596
|
+
tx.partialSign(kp);
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
374
599
|
const roundTwo = await core.buildSetDocumentsTransaction(connection, sol.txArgs[0].idl, anchorProvider, kp.publicKey, {
|
|
375
600
|
app_id,
|
|
376
601
|
documents: sol.txArgs[0].setDocumentData,
|
|
@@ -380,8 +605,8 @@ class SolanaKeypairProvider {
|
|
|
380
605
|
tx = roundTwo.tx;
|
|
381
606
|
blockhash = roundTwo.blockhash;
|
|
382
607
|
lastValidBlockHeight = roundTwo.lastValidBlockHeight;
|
|
383
|
-
tx.sign([kp]);
|
|
384
608
|
tx.message.recentBlockhash = blockhash;
|
|
609
|
+
tx.sign([kp]);
|
|
385
610
|
}
|
|
386
611
|
}
|
|
387
612
|
}
|
|
@@ -452,9 +677,6 @@ class OffchainAuthProvider {
|
|
|
452
677
|
}
|
|
453
678
|
|
|
454
679
|
let currentAuthProvider = null;
|
|
455
|
-
const SOLANA_DEVNET_RPC_URL = "https://idelle-8nxsep-fast-devnet.helius-rpc.com";
|
|
456
|
-
const SOLANA_MAINNET_RPC_URL = "https://celestia-cegncv-fast-mainnet.helius-rpc.com";
|
|
457
|
-
const SURFNET_RPC_URL = "https://surfpool.fly.dev";
|
|
458
680
|
async function getAuthProvider() {
|
|
459
681
|
var _a;
|
|
460
682
|
const config = await core.getConfig();
|