@solana/web3.js 2.0.0-preview.4.20240731091009.c2717fc498afa3228c8f8b01600a575989afae99 → 2.0.0-rc.1
Sign up to get free protection for your applications and to get access to all the features.
- package/README.md +76 -1337
- package/dist/index.development.js +111 -5
- package/dist/index.development.js.map +1 -1
- package/dist/index.production.min.js +534 -531
- package/package.json +20 -20
package/README.md
CHANGED
@@ -6,861 +6,57 @@
|
|
6
6
|
|
7
7
|
[code-style-prettier-image]: https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square
|
8
8
|
[code-style-prettier-url]: https://github.com/prettier/prettier
|
9
|
-
[npm-downloads-image]: https://img.shields.io/npm/dm/@solana/web3.js/
|
10
|
-
[npm-image]: https://img.shields.io/npm/v/@solana/web3.js/
|
11
|
-
[npm-url]: https://www.npmjs.com/package/@solana/web3.js/v/
|
9
|
+
[npm-downloads-image]: https://img.shields.io/npm/dm/@solana/web3.js/rc.svg?style=flat
|
10
|
+
[npm-image]: https://img.shields.io/npm/v/@solana/web3.js/rc.svg?style=flat
|
11
|
+
[npm-url]: https://www.npmjs.com/package/@solana/web3.js/v/rc
|
12
12
|
[semantic-release-image]: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg
|
13
13
|
[semantic-release-url]: https://github.com/semantic-release/semantic-release
|
14
14
|
|
15
|
-
#
|
15
|
+
# @solana/web3.js
|
16
16
|
|
17
|
-
|
17
|
+
This is the JavaScript SDK for building Solana apps for Node, web, and React Native.
|
18
18
|
|
19
|
-
|
19
|
+
## Functions
|
20
20
|
|
21
|
-
|
22
|
-
const connection = new Connection('https://api.mainnet-beta.solana.com');
|
23
|
-
const instruction = SystemProgram.transfer({ fromPubkey, toPubkey, lamports });
|
24
|
-
const transaction = new Transaction().add(instruction);
|
25
|
-
await sendAndConfirmTransaction(connection, transaction, [payer]);
|
26
|
-
```
|
27
|
-
|
28
|
-
In response to your feedback, we began a process of modernizing the library to prepare for the next generation of Solana applications. A Release Candidate of the new web3.js is now available for you to evaluate.
|
29
|
-
|
30
|
-
Before leaving the Release Candidate stage, we want to collect as much of your feeback and bug reports as possible. If you find a bug or can not achieve your goals because of the design of the new APIs, please file [a GitHub issue here](https://github.com/solana-labs/solana-web3.js/issues/new/choose).
|
31
|
-
|
32
|
-
# Installation
|
33
|
-
|
34
|
-
For use in a Node.js or web application:
|
35
|
-
|
36
|
-
```shell
|
37
|
-
npm install --save @solana/web3.js@rc
|
38
|
-
```
|
39
|
-
|
40
|
-
For use in a browser, without a build system:
|
41
|
-
|
42
|
-
```html
|
43
|
-
<!-- Development (debug mode, unminified) -->
|
44
|
-
<script src="https://unpkg.com/@solana/web3.js@rc/dist/index.development.js"></script>
|
45
|
-
|
46
|
-
<!-- Production (minified) -->
|
47
|
-
<script src="https://unpkg.com/@solana/web3.js@rc/dist/index.production.min.js"></script>
|
48
|
-
```
|
49
|
-
|
50
|
-
# Examples
|
51
|
-
|
52
|
-
To get a feel for the new API, run and modify the live examples in the `examples/` directory. There, you will find a series of single-purpose Node scripts that demonstrate a specific feature or use case. You will also find a React application that you can run in a browser, that demonstrates being able to create, sign, and send transactions using browser wallets.
|
53
|
-
|
54
|
-
# What's New
|
55
|
-
|
56
|
-
Version 2.0 of the Solana JavaScript SDK is a response to many of the pain points you have communicated to us when developing Solana applications with web3.js. We’ve heard you loud and clear.
|
57
|
-
|
58
|
-
## Tree-Shakability
|
59
|
-
|
60
|
-
The object-oriented design of the web3.js (1.x) API prevents optimizing compilers from being able to ‘tree-shake’ unused code from your production builds. No matter how much of the web3.js API you use in your application, you have until now been forced to package all of it.
|
61
|
-
|
62
|
-
Read more about tree-shaking here:
|
63
|
-
|
64
|
-
- [Mozilla Developer Docs: Tree Shaking](https://developer.mozilla.org/en-US/docs/Glossary/Tree_shaking)
|
65
|
-
- [WebPack Docs: Tree Shaking](https://webpack.js.org/guides/tree-shaking/)
|
66
|
-
- [Web.Dev Blog Article: Reduce JavaScript Payloads with Tree Shaking](https://web.dev/articles/reduce-javascript-payloads-with-tree-shaking)
|
67
|
-
|
68
|
-
One example of an API that can’t be tree-shaken is the `Connection` class. It has dozens of methods, but because it’s a _class_ you have no choice but to include every method in your application’s final bundle, no matter how many you _actually_ use.
|
69
|
-
|
70
|
-
Needlessly large JavaScript bundles can cause issues with deployments to cloud compute providers like Cloudflare or AWS Lambda. They also impact webapp startup performance because of longer download and JavaScript parse times.
|
71
|
-
|
72
|
-
Version 2.0 is fully tree-shakable and will remain so, enforced by build-time checks. Optimizing compilers can now eliminate those parts of the library that your application does not use.
|
73
|
-
|
74
|
-
The new library itself is comprised of several smaller, modular packages under the `@solana` organization, including:
|
75
|
-
|
76
|
-
- `@solana/accounts`: For fetching and decoding accounts
|
77
|
-
- `@solana/codecs`: For composing data (de)serializers from a set of primitives or building custom ones
|
78
|
-
- `@solana/errors`: For identifying and refining coded errors thrown in the `@solana` namespace
|
79
|
-
- `@solana/rpc`: For sending RPC requests
|
80
|
-
- `@solana/rpc-subscriptions`: For subscribing to RPC notifications
|
81
|
-
- `@solana/signers`: For building message and/or transaction signer objects
|
82
|
-
- `@solana/sysvars`: For fetching and decoding sysvar accounts
|
83
|
-
- `@solana/transaction-messages`: For building and transforming Solana transaction message objects
|
84
|
-
- `@solana/transactions`: For compiling and signing transactions for submission to the network
|
85
|
-
- And many more!
|
86
|
-
|
87
|
-
Some of these packages are themselves composed of smaller packages. For instance, `@solana/rpc` is composed of `@solana/rpc-spec` (for core JSON RPC specification types), `@solana/rpc-api` (for the Solana-specific RPC methods), `@solana/rpc-transport-http` (for the default HTTP transport) and so on.
|
88
|
-
|
89
|
-
Developers can use the default configurations within the main library (`@solana/web3.js@rc`) or import any of its subpackages where customization-through-composition is desired.
|
90
|
-
|
91
|
-
## Composable Internals
|
92
|
-
|
93
|
-
Depending on your use case and your tolerance for certain application behaviours, you may wish to configure your application to make a different set of tradeoffs than another developer. The web3.js (1.x) API imposed a rigid set of common-case defaults on _all_ developers, some of which were impossible to change.
|
94
|
-
|
95
|
-
The inability to customize web3.js up until now has been a source of frustration:
|
96
|
-
|
97
|
-
- The Mango team wanted to customize the transaction confirmation strategy, but all of that functionality is hidden away behind `confirmTransaction` – a static method of `Connection`. [Here’s the code for `confirmTransaction` on GitHub](https://github.com/solana-labs/solana-web3.js/blob/69a8ad25ef09f9e6d5bff1ffa8428d9be0bd32ac/packages/library-legacy/src/connection.ts#L3734).
|
98
|
-
- Solana developer ‘mPaella’ [wanted us to add a feature in the RPC](https://github.com/solana-labs/solana-web3.js/issues/1143#issuecomment-1435927152) that would failover to a set of backup URLs in case the primary one failed.
|
99
|
-
- Solana developer ‘epicfaace’ wanted first-class support for automatic time-windowed batching in the RPC transport. [Here’s their pull request](https://github.com/solana-labs/solana/pull/23628).
|
100
|
-
- Multiple folks have expressed the need for custom retry logic for failed requests or transactions. [Here’s a pull request from ‘dafyddd’](https://github.com/solana-labs/solana/pull/11811) and [another from ‘abrkn’](https://github.com/solana-labs/solana-web3.js/issues/1041) attempting to modify retry logic to suit their individual use cases.
|
101
|
-
|
102
|
-
Version 2.0 exposes far more of its internals, particularly where communication with an RPC is concerned, and allows willing developers the ability to compose new implementations from the default ones that manifest a nearly limitless array of customizations.
|
103
|
-
|
104
|
-
The individual modules that make up web3.js are assembled in a **default** configuration reminiscent of the legacy library as part of the npm package `@solana/web3.js@rc`, but those who wish to assemble them in different configurations may do so.
|
105
|
-
|
106
|
-
Generic types are offered in numerous places, allowing you to specify new functionality, to make extensions to each API via composition and supertypes, and to encourage you to create higher-level opinionated abstractions of your own.
|
107
|
-
|
108
|
-
In fact, we expect you to do so, and to open source some of those for use by others with similar needs.
|
109
|
-
|
110
|
-
## Modern JavaScript; Zero-Dependency
|
111
|
-
|
112
|
-
The advance of modern JavaScript features presents an opportunity to developers of crypto applications, such as the ability to use native Ed25519 keys and to express large values as native `bigint`.
|
113
|
-
|
114
|
-
The Web Incubator Community Group has advocated for the addition of Ed25519 support to the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API), and support has already landed in _most_ modern JavaScript runtimes.
|
115
|
-
|
116
|
-
Engine support for `bigint` values has also become commonplace. The older `number` primitive in JavaScript has a maximum value of 2^53 - 1, whereas Rust’s `u64` can represent values up to 2^64.
|
117
|
-
|
118
|
-
Version 2.0 eliminates userspace implementations of Ed25519 cryptography, large number polyfills, and more, in favour of custom implementations or the use of native JavaScript features, reducing the size of the library. It has no third-party dependencies.
|
119
|
-
|
120
|
-
## Functional Architecture
|
121
|
-
|
122
|
-
The object oriented, class-based architecture of web3.js (1.x) causes unnecessary bundle bloat. Your application has no choice but to bundle _all_ of the functionality and dependencies of a class no matter how many methods you actually use at runtime.
|
123
|
-
|
124
|
-
Class-based architecture also presents unique risks to developers who trigger the dual-package hazard. This describes a situation you can find yourself in if you build for both CommonJS and ES modules. It arises when two copies of the same class are present in the dependency tree, causing checks like `instanceof` to fail. This introduces aggravating and difficult to debug problems.
|
125
|
-
|
126
|
-
Read more about dual-package hazard:
|
127
|
-
|
128
|
-
- [NodeJS: Dual Package Hazard](https://nodejs.org/api/packages.html#dual-package-hazard)
|
129
|
-
|
130
|
-
Version 2.0 implements no classes (with the notable exception of the `SolanaError` class) and implements the thinnest possible interfaces at function boundaries.
|
131
|
-
|
132
|
-
## Statistics
|
133
|
-
|
134
|
-
Consider these statistical comparisons between version 2.0 and the legacy 1.x.
|
135
|
-
|
136
|
-
| | 1.x (Legacy) | 2.0 | +/- % |
|
137
|
-
| ------------------------------------------------------------------------------------------------------ | ------------ | ---------- | ----- |
|
138
|
-
| Total minified size of library | 81 KB | 57.5 KB | -29% |
|
139
|
-
| Total minified size of library (when runtime supports Ed25519) | 81 KB | 53 KB | -33% |
|
140
|
-
| Bundled size of a web application that executes a transfer of lamports | 111 KB | 23.9 KB | -78% |
|
141
|
-
| Bundled size of a web application that executes a transfer of lamports (when runtime supports Ed25519) | 111 KB | 18.2 KB | -83% |
|
142
|
-
| Performance of key generation, signing, and verifying signatures (Brave with Experimental API flag) | 700 ops/s | 7000 ops/s | +900% |
|
143
|
-
| First-load size for Solana Explorer | 311 KB | 228 KB | -26% |
|
144
|
-
|
145
|
-
The re-engineered library achieves these speedups and reductions in bundle size in large part through use of modern JavaScript APIs.
|
146
|
-
|
147
|
-
To validate our work, we replaced the legacy 1.x library with the new 2.0 library on the homepage of the Solana Explorer. Total first-load bundle size dropped by 26% without removing a single feature. [Here’s an X thread](https://twitter.com/callum_codes/status/1679124485218226176) by Callum McIntyre if you would like to dig deeper.
|
148
|
-
|
149
|
-
# A Tour of the Version 2.0 API
|
150
|
-
|
151
|
-
Here’s an overview of how to use the new library to interact with the RPC, configure network transports, work with Ed25519 keys, and to serialize data.
|
152
|
-
|
153
|
-
## RPC
|
154
|
-
|
155
|
-
Version 2.0 ships with an implementation of the [JSON RPC specification](https://www.jsonrpc.org/specification) and a type spec for the [Solana JSON RPC](https://docs.solana.com/api).
|
156
|
-
|
157
|
-
The main package responsible for managing communication with an RPC is `@solana/rpc`. However, this package makes use of more granular packages to break down the RPC logic into smaller pieces. Namely, these packages are:
|
158
|
-
|
159
|
-
- `@solana/rpc`: Contains all logic related to sending Solana RPC calls.
|
160
|
-
- `@solana/rpc-api`: Describes all Solana RPC methods using types.
|
161
|
-
- `@solana/rpc-transport-http`: Provides a concrete implementation of an RPC transport using HTTP requests.
|
162
|
-
- `@solana/rpc-spec`: Defines the JSON RPC spec for sending RPC requests.
|
163
|
-
- `@solana/rpc-spec-types`: Shared JSON RPC specifications types and helpers that are used by both `@solana/rpc` and `@solana/rpc-subscriptions` (described in the next section).
|
164
|
-
- `@solana/rpc-types`: Shared Solana RPC types and helpers that are used by both `@solana/rpc` and `@solana/rpc-subscriptions`.
|
165
|
-
|
166
|
-
The main `@solana/web3.js` package re-exports the `@solana/rpc` package so, going forward, we will import RPC types and functions from the library directly.
|
167
|
-
|
168
|
-
### RPC Calls
|
169
|
-
|
170
|
-
You can use the `createSolanaRpc` function by providing the URL of a Solana JSON RPC server. This will create a default client for interacting with the Solana JSON RPC API.
|
171
|
-
|
172
|
-
```ts
|
173
|
-
import { createSolanaRpc } from '@solana/web3.js';
|
174
|
-
|
175
|
-
// Create an RPC client.
|
176
|
-
const rpc = createSolanaRpc('http://127.0.0.1:8899');
|
177
|
-
// ^? Rpc<SolanaRpcApi>
|
178
|
-
|
179
|
-
// Send a request.
|
180
|
-
const slot = await rpc.getSlot().send();
|
181
|
-
```
|
182
|
-
|
183
|
-
### Custom RPC Transports
|
184
|
-
|
185
|
-
The `createSolanaRpc` function communicates with the RPC server using a default HTTP transport that should satisfy most use cases. You can provide your own transport or wrap an existing one to communicate with RPC servers in any way you see fit. In the example below, we explicitly create a transport and use it to create a new RPC client via the `createSolanaRpcFromTransport` function.
|
186
|
-
|
187
|
-
```ts
|
188
|
-
import { createSolanaRpcFromTransport, createDefaultRpcTransport } from '@solana/web3.js';
|
189
|
-
|
190
|
-
// Create an HTTP transport or any custom transport of your choice.
|
191
|
-
const transport = createDefaultRpcTransport({ url: 'https://api.devnet.solana.com' });
|
192
|
-
|
193
|
-
// Create an RPC client using that transport.
|
194
|
-
const rpc = createSolanaRpcFromTransport(transport);
|
195
|
-
// ^? Rpc<SolanaRpcApi>
|
196
|
-
|
197
|
-
// Send a request.
|
198
|
-
const slot = await rpc.getSlot().send();
|
199
|
-
```
|
200
|
-
|
201
|
-
A custom transport can implement specialized functionality such as coordinating multiple transports, implementing retries, and more. Let's take a look at some concrete examples.
|
202
|
-
|
203
|
-
#### Round Robin
|
204
|
-
|
205
|
-
A ‘round robin’ transport is one that distributes requests to a list of endpoints in sequence.
|
206
|
-
|
207
|
-
```ts
|
208
|
-
import { createDefaultRpcTransport, createSolanaRpcFromTransport, type RpcTransport } from '@solana/web3.js';
|
209
|
-
|
210
|
-
// Create an HTTP transport for each RPC server.
|
211
|
-
const transports = [
|
212
|
-
createDefaultRpcTransport({ url: 'https://mainnet-beta.my-server-1.com' }),
|
213
|
-
createDefaultRpcTransport({ url: 'https://mainnet-beta.my-server-2.com' }),
|
214
|
-
createDefaultRpcTransport({ url: 'https://mainnet-beta.my-server-3.com' }),
|
215
|
-
];
|
216
|
-
|
217
|
-
// Set up the round-robin transport.
|
218
|
-
let nextTransport = 0;
|
219
|
-
async function roundRobinTransport<TResponse>(...args: Parameters<RpcTransport>): Promise<TResponse> {
|
220
|
-
const transport = transports[nextTransport];
|
221
|
-
nextTransport = (nextTransport + 1) % transports.length;
|
222
|
-
return await transport(...args);
|
223
|
-
}
|
224
|
-
|
225
|
-
// Create an RPC client using the round-robin transport.
|
226
|
-
const rpc = createSolanaRpcFromTransport(roundRobinTransport);
|
227
|
-
```
|
228
|
-
|
229
|
-
#### Sharding
|
230
|
-
|
231
|
-
A sharding transport is a kind of distributing transport that sends requests to a particular server based on something about the request itself. Here’s an example that sends requests to different servers depending on the name of the method:
|
232
|
-
|
233
|
-
```ts
|
234
|
-
import { createDefaultRpcTransport, createSolanaRpcFromTransport, type RpcTransport } from '@solana/web3.js';
|
235
|
-
|
236
|
-
// Create multiple transports.
|
237
|
-
const transportA = createDefaultRpcTransport({ url: 'https://mainnet-beta.my-server-1.com' });
|
238
|
-
const transportB = createDefaultRpcTransport({ url: 'https://mainnet-beta.my-server-2.com' });
|
239
|
-
const transportC = createDefaultRpcTransport({ url: 'https://mainnet-beta.my-server-3.com' });
|
240
|
-
const transportD = createDefaultRpcTransport({ url: 'https://mainnet-beta.my-server-4.com' });
|
241
|
-
|
242
|
-
// Function to determine which shard to use based on the request method.
|
243
|
-
function selectShard(method: string): RpcTransport {
|
244
|
-
switch (method) {
|
245
|
-
case 'getAccountInfo':
|
246
|
-
case 'getBalance':
|
247
|
-
return transportA;
|
248
|
-
case 'getTransaction':
|
249
|
-
case 'getRecentBlockhash':
|
250
|
-
return transportB;
|
251
|
-
case 'sendTransaction':
|
252
|
-
return transportC;
|
253
|
-
default:
|
254
|
-
return transportD;
|
255
|
-
}
|
256
|
-
}
|
257
|
-
|
258
|
-
// Create a transport that selects the correct transport given the request method name.
|
259
|
-
async function shardingTransport<TResponse>(...args: Parameters<RpcTransport>): Promise<TResponse> {
|
260
|
-
const payload = args[0].payload as { method: string };
|
261
|
-
const selectedTransport = selectShard(payload.method);
|
262
|
-
return (await selectedTransport(...args)) as TResponse;
|
263
|
-
}
|
264
|
-
|
265
|
-
// Create an RPC client using the sharding transport.
|
266
|
-
const rpc = createSolanaRpcFromTransport(shardingTransport);
|
267
|
-
```
|
268
|
-
|
269
|
-
#### Retry
|
270
|
-
|
271
|
-
A custom transport is a good place to implement global retry logic for every request:
|
272
|
-
|
273
|
-
```ts
|
274
|
-
import { createDefaultRpcTransport, createSolanaRpcFromTransport, type RpcTransport } from '@solana/web3.js';
|
275
|
-
|
276
|
-
// Set the maximum number of attempts to retry a request.
|
277
|
-
const MAX_ATTEMPTS = 4;
|
278
|
-
|
279
|
-
// Create the default transport.
|
280
|
-
const defaultTransport = createDefaultRpcTransport({ url: 'https://mainnet-beta.my-server-1.com' });
|
281
|
-
|
282
|
-
// Sleep function to wait for a given number of milliseconds.
|
283
|
-
function sleep(ms: number): Promise<void> {
|
284
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
285
|
-
}
|
286
|
-
|
287
|
-
// Calculate the delay for a given attempt.
|
288
|
-
function calculateRetryDelay(attempt: number): number {
|
289
|
-
// Exponential backoff with a maximum of 1.5 seconds.
|
290
|
-
return Math.min(100 * Math.pow(2, attempt), 1500);
|
291
|
-
}
|
292
|
-
|
293
|
-
// A retrying transport that will retry up to MAX_ATTEMPTS times before failing.
|
294
|
-
async function retryingTransport<TResponse>(...args: Parameters<RpcTransport>): Promise<TResponse> {
|
295
|
-
let requestError;
|
296
|
-
for (let attempts = 0; attempts < MAX_ATTEMPTS; attempts++) {
|
297
|
-
try {
|
298
|
-
return await defaultTransport(...args);
|
299
|
-
} catch (err) {
|
300
|
-
requestError = err;
|
301
|
-
// Only sleep if we have more attempts remaining.
|
302
|
-
if (attempts < MAX_ATTEMPTS - 1) {
|
303
|
-
const retryDelay = calculateRetryDelay(attempts);
|
304
|
-
await sleep(retryDelay);
|
305
|
-
}
|
306
|
-
}
|
307
|
-
}
|
308
|
-
throw requestError;
|
309
|
-
}
|
310
|
-
|
311
|
-
// Create the RPC client using the retrying transport.
|
312
|
-
const rpc = createSolanaRpcFromTransport(retryingTransport);
|
313
|
-
```
|
314
|
-
|
315
|
-
#### Failover
|
316
|
-
|
317
|
-
Support for handling network failures can be implemented in the transport itself. Here’s an example of some failover logic integrated into a transport:
|
318
|
-
|
319
|
-
```ts
|
320
|
-
// TODO: Your turn; send us a pull request with an example.
|
321
|
-
```
|
322
|
-
|
323
|
-
### Augmenting/Constraining the RPC API
|
324
|
-
|
325
|
-
Using the `createSolanaRpc` or `createSolanaRpcFromTransport` methods, we always get the same API that includes the Solana RPC API methods. Since the RPC API is described using types only, it is possible to augment those types to add your own methods.
|
326
|
-
|
327
|
-
When constraining the API scope, keep in mind that types don’t affect bundle size. You may still like to constrain the type-spec for a variety of reasons, including reducing TypeScript noise.
|
328
|
-
|
329
|
-
#### Constraining by Cluster
|
330
|
-
|
331
|
-
If you're using a specific cluster, you may wrap your RPC URL inside a helper function like `mainnet` or `devnet` to inject that information into the RPC type system.
|
332
|
-
|
333
|
-
```ts
|
334
|
-
import { createSolanaRpc, mainnet, devnet } from '@solana/web3.js';
|
335
|
-
|
336
|
-
const mainnetRpc = createSolanaRpc(mainnet('https://api.mainnet-beta.solana.com'));
|
337
|
-
// ^? RpcMainnet<SolanaRpcApiMainnet>
|
338
|
-
|
339
|
-
const devnetRpc = createSolanaRpc(devnet('https://api.devnet.solana.com'));
|
340
|
-
// ^? RpcDevnet<SolanaRpcApiDevnet>
|
341
|
-
```
|
342
|
-
|
343
|
-
In the example above, `devnetRpc.requestAirdrop(..)` will work, but `mainnetRpc.requestAirdrop(..)` will raise a TypeScript error since `requestAirdrop` is not a valid method of the mainnet cluster.
|
344
|
-
|
345
|
-
#### Cherry-Picking API Methods
|
346
|
-
|
347
|
-
You can constrain the API’s type-spec even further so you are left only with the methods you need. The simplest way to do this is to cast the created RPC client to a type that only includes the required methods.
|
348
|
-
|
349
|
-
```ts
|
350
|
-
import { createSolanaRpc, type Rpc, type GetAccountInfoApi, type GetMultipleAccountsApi } from '@solana/web3.js';
|
351
|
-
|
352
|
-
const rpc = createSolanaRpc('http://127.0.0.1:8899') as Rpc<GetAccountInfoApi & GetMultipleAccountsApi>;
|
353
|
-
```
|
354
|
-
|
355
|
-
Alternatively, you can explicitly create the RPC API using the `createSolanaRpcApi` function. You will need to create your own transport and bind the two together using the `createRpc` function.
|
356
|
-
|
357
|
-
```ts
|
358
|
-
import {
|
359
|
-
createDefaultRpcTransport,
|
360
|
-
createRpc,
|
361
|
-
createSolanaRpcApi,
|
362
|
-
DEFAULT_RPC_CONFIG,
|
363
|
-
type GetAccountInfoApi,
|
364
|
-
type GetMultipleAccountsApi,
|
365
|
-
} from '@solana/web3.js';
|
366
|
-
|
367
|
-
const api = createSolanaRpcApi<GetAccountInfoApi & GetMultipleAccountsApi>(DEFAULT_RPC_CONFIG);
|
368
|
-
const transport = createDefaultRpcTransport({ url: 'http:127.0.0.1:8899' });
|
369
|
-
|
370
|
-
const rpc = createRpc({ api, transport });
|
371
|
-
```
|
372
|
-
|
373
|
-
Note that the `createSolanaRpcApi` function is a wrapper on top of the `createRpcApi` function which adds some Solana-specific transformers such as setting a default commitment on all methods or throwing an error when an integer overflow is detected.
|
374
|
-
|
375
|
-
#### Creating Your Own API Methods
|
376
|
-
|
377
|
-
The new library’s RPC specification supports an _infinite_ number of JSON-RPC methods with **zero increase** in bundle size.
|
378
|
-
|
379
|
-
This means the library can support future additions to the official [Solana JSON RPC](https://docs.solana.com/api), or [custom RPC methods](https://docs.helius.dev/compression-and-das-api/digital-asset-standard-das-api/get-asset) defined by some RPC provider.
|
380
|
-
|
381
|
-
Here’s an example of how a developer at might build a custom RPC type-spec for an RPC provider's implementation of the Metaplex Digital Asset Standard's `getAsset` method:
|
382
|
-
|
383
|
-
```ts
|
384
|
-
import { RpcApiMethods } from '@solana/web3.js';
|
385
|
-
|
386
|
-
// Define the method's response payload.
|
387
|
-
type GetAssetApiResponse = Readonly<{
|
388
|
-
interface: DasApiAssetInterface;
|
389
|
-
id: Address;
|
390
|
-
content: Readonly<{
|
391
|
-
files?: readonly {
|
392
|
-
mime?: string;
|
393
|
-
uri?: string;
|
394
|
-
[key: string]: unknown;
|
395
|
-
}[];
|
396
|
-
json_uri: string;
|
397
|
-
links?: readonly {
|
398
|
-
[key: string]: unknown;
|
399
|
-
}[];
|
400
|
-
metadata: DasApiMetadata;
|
401
|
-
}>;
|
402
|
-
/* ...etc... */
|
403
|
-
}>;
|
404
|
-
|
405
|
-
// Set up an interface for the request method.
|
406
|
-
interface GetAssetApi extends RpcApiMethods {
|
407
|
-
// Define the method's name, parameters and response type
|
408
|
-
getAsset(args: { id: Address }): GetAssetApiResponse;
|
409
|
-
}
|
410
|
-
|
411
|
-
// Export the type spec for downstream users.
|
412
|
-
export type MetaplexDASApi = GetAssetApi;
|
413
|
-
```
|
414
|
-
|
415
|
-
Here’s how a developer might use it:
|
416
|
-
|
417
|
-
```ts
|
418
|
-
import { createDefaultRpcTransport, createRpc, createRpcApi } from '@solana/web3.js';
|
419
|
-
|
420
|
-
// Create the custom API.
|
421
|
-
const api = createRpcApi<MetaplexDASApi>();
|
422
|
-
|
423
|
-
// Set up an HTTP transport to a server that supports the custom API.
|
424
|
-
const transport = createDefaultRpcTransport({
|
425
|
-
url: 'https://mainnet.helius-rpc.com/?api-key=<api_key>',
|
426
|
-
});
|
427
|
-
|
428
|
-
// Create the RPC client.
|
429
|
-
const metaplexDASRpc = createRpc({ api, transport });
|
430
|
-
// ^? Rpc<MetaplexDASApi>
|
431
|
-
```
|
432
|
-
|
433
|
-
As long as a particular JSON RPC method adheres to the [official JSON RPC specification](https://www.jsonrpc.org/specification), it will be supported by version 2.0.
|
434
|
-
|
435
|
-
### Aborting RPC Requests
|
436
|
-
|
437
|
-
RPC requests are now abortable with modern `AbortControllers`. When calling an RPC method such as `getSlot`, it will return a `PendingRpcRequest` proxy object that contains a `send` method to send the request to the server.
|
438
|
-
|
439
|
-
```ts
|
440
|
-
const pendingRequest: PendingRpcRequest<Slot> = rpc.getSlot();
|
441
|
-
|
442
|
-
const slot: Slot = await pendingRequest.send();
|
443
|
-
```
|
444
|
-
|
445
|
-
The arguments of the `getSlot` method are reserved for the request payload, but the `send` method is where additional arguments such as an `AbortSignal` can be accepted in the context of the request.
|
446
|
-
|
447
|
-
Aborting RPC requests can be useful for a variety of things such as setting a timeout on a request or cancelling a request when a user navigates away from a page.
|
448
|
-
|
449
|
-
```ts
|
450
|
-
import { createSolanaRpc } from '@solana/web3.js';
|
451
|
-
|
452
|
-
const rpc = createSolanaRpc('http://127.0.0.1:8900');
|
453
|
-
|
454
|
-
// Create a new AbortController.
|
455
|
-
const abortController = new AbortController();
|
456
|
-
|
457
|
-
// Abort the request when the user navigates away from the current page.
|
458
|
-
function onUserNavigateAway() {
|
459
|
-
abortController.abort();
|
460
|
-
}
|
461
|
-
|
462
|
-
// The request will be aborted if and only if the user navigates away from the page.
|
463
|
-
const slot = await rpc.getSlot().send({ abortSignal: abortController.signal });
|
464
|
-
```
|
465
|
-
|
466
|
-
Read more about `AbortController` here:
|
467
|
-
|
468
|
-
- [Mozilla Developer Docs: `AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController)
|
469
|
-
- [Mozilla Developer Docs: `AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal)
|
470
|
-
- [JavaScript.info: Fetch: Abort](https://javascript.info/fetch-abort)
|
471
|
-
|
472
|
-
## RPC Subscriptions
|
473
|
-
|
474
|
-
Subscriptions in the legacy library do not allow custom retry logic and do not allow you to recover from potentially missed messages. The new version does away with silent retries, surfaces transport errors to your application, and gives you the opportunity to recover from gap events.
|
475
|
-
|
476
|
-
The main package responsible for managing communication with RPC subscriptions is `@solana/rpc-subscriptions`. However, similarly to `@solana/rpc`, this package also makes use of more granular packages. These packages are:
|
477
|
-
|
478
|
-
- `@solana/rpc-subscriptions`: Contains all logic related to subscribing to Solana RPC notifications.
|
479
|
-
- `@solana/rpc-subscriptions-api`: Describes all Solana RPC subscriptions using types.
|
480
|
-
- `@solana/rpc-subscriptions-transport-websocket`: Provides a concrete implementation of an RPC Subscriptions transport using WebSockets.
|
481
|
-
- `@solana/rpc-subscriptions-spec`: Defines the JSON RPC spec for subscribing to RPC notifications.
|
482
|
-
- `@solana/rpc-spec-types`: Shared JSON RPC specifications types and helpers that are used by both `@solana/rpc` and `@solana/rpc-subscriptions`.
|
483
|
-
- `@solana/rpc-types`: Shared Solana RPC types and helpers that are used by both `@solana/rpc` and `@solana/rpc-subscriptions`.
|
484
|
-
|
485
|
-
Since the main `@solana/web3.js` library also re-exports the `@solana/rpc-subscriptions` package we will import RPC Subscriptions types and functions directly from the main library going forward.
|
486
|
-
|
487
|
-
### Getting Started with RPC Subscriptions
|
488
|
-
|
489
|
-
To get started with RPC Subscriptions, you may use the `createSolanaRpcSubscriptions` function by providing the WebSocket URL of a Solana JSON RPC server. This will create a default client for interacting with Solana RPC Subscriptions.
|
490
|
-
|
491
|
-
```ts
|
492
|
-
import { createSolanaRpcSubscriptions } from '@solana/web3.js';
|
493
|
-
|
494
|
-
// Create an RPC Subscriptions client.
|
495
|
-
const rpcSubscriptions = createSolanaRpcSubscriptions('ws://127.0.0.1:8900');
|
496
|
-
// ^? RpcSubscriptions<SolanaRpcSubscriptionsApi>
|
497
|
-
```
|
498
|
-
|
499
|
-
### Subscriptions as `AsyncIterators`
|
500
|
-
|
501
|
-
The new subscriptions API vends subscription notifications as an `AsyncIterator`. The `AsyncIterator` conforms to the [async iterator protocol](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_async_iterator_and_async_iterable_protocols), which allows developers to consume messages using a `for await...of` loop.
|
502
|
-
|
503
|
-
Here’s an example of working with a subscription in the new library:
|
504
|
-
|
505
|
-
```ts
|
506
|
-
import { address, createSolanaRpcSubscriptions, createDefaultRpcSubscriptionsTransport } from '@solana/web3.js';
|
507
|
-
|
508
|
-
// Create the RPC Subscriptions client.
|
509
|
-
const rpcSubscriptions = createSolanaRpcSubscriptions('ws://127.0.0.1:8900');
|
510
|
-
|
511
|
-
// Set up an abort controller.
|
512
|
-
const abortController = new AbortController();
|
513
|
-
|
514
|
-
// Subscribe to account notifications.
|
515
|
-
const accountNotifications = await rpcSubscriptions
|
516
|
-
.accountNotifications(address('AxZfZWeqztBCL37Mkjkd4b8Hf6J13WCcfozrBY6vZzv3'), { commitment: 'confirmed' })
|
517
|
-
.subscribe({ abortSignal: abortController.signal });
|
518
|
-
|
519
|
-
try {
|
520
|
-
// Consume messages.
|
521
|
-
for await (const notification of accountNotifications) {
|
522
|
-
console.log('New balance', notification.value.lamports);
|
523
|
-
}
|
524
|
-
} catch (e) {
|
525
|
-
// The subscription went down.
|
526
|
-
// Retry it and then recover from potentially having missed
|
527
|
-
// a balance update, here (eg. by making a `getBalance()` call).
|
528
|
-
}
|
529
|
-
```
|
530
|
-
|
531
|
-
You can read more about `AsyncIterator` at the following links:
|
532
|
-
|
533
|
-
- [Mozilla Developer Docs: `AsyncIterator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncIterator)
|
534
|
-
- [Luciano Mammino (Blog): JavaScript Async Iterators](https://www.nodejsdesignpatterns.com/blog/javascript-async-iterators/)
|
535
|
-
|
536
|
-
### Aborting RPC Subscriptions
|
537
|
-
|
538
|
-
Similarly to RPC calls, applications can terminate active subscriptions using an `AbortController` attribute on the `subscribe` method. In fact, this parameter is _required_ for subscriptions to encourage you to clean up subscriptions that your application no longer needs.
|
539
|
-
|
540
|
-
Let's take a look at some concrete examples that demonstrate how to abort subscriptions.
|
541
|
-
|
542
|
-
#### Subscription Timeout
|
543
|
-
|
544
|
-
Here's an example of an `AbortController` used to abort a subscription after a 5-second timeout:
|
545
|
-
|
546
|
-
```ts
|
547
|
-
import { createSolanaRpcSubscriptions } from '@solana/web3.js';
|
548
|
-
|
549
|
-
const rpcSubscriptions = createSolanaRpcSubscriptions('ws://127.0.0.1:8900');
|
550
|
-
|
551
|
-
// Subscribe for slot notifications using an AbortSignal that times out after 5 seconds.
|
552
|
-
const slotNotifications = await rpcSubscriptions
|
553
|
-
.slotNotifications()
|
554
|
-
.subscribe({ abortSignal: AbortSignal.timeout(5000) });
|
555
|
-
|
556
|
-
// Log slot notifications.
|
557
|
-
for await (const notification of slotNotifications) {
|
558
|
-
console.log('Slot notification', notification);
|
559
|
-
}
|
560
|
-
|
561
|
-
console.log('Done.');
|
562
|
-
```
|
563
|
-
|
564
|
-
Read more about `AbortController` at the following links:
|
565
|
-
|
566
|
-
- [Mozilla Developer Docs: `AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController)
|
567
|
-
- [Mozilla Developer Docs: `AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal)
|
568
|
-
- [JavaScript.info: Fetch: Abort](https://javascript.info/fetch-abort)
|
569
|
-
|
570
|
-
#### Cancelling Subscriptions
|
571
|
-
|
572
|
-
It is also possible to abort a subscription inside the `for await...of` loop. This enables us to cancel a subscription based on some condition, such as a change in the state of an account. For instance, the following example cancels a subscription when the owner of an account changes:
|
573
|
-
|
574
|
-
```ts
|
575
|
-
// Subscribe to account notifications.
|
576
|
-
const accountNotifications = await rpc
|
577
|
-
.accountNotifications(address('AxZfZWeqztBCL37Mkjkd4b8Hf6J13WCcfozrBY6vZzv3'), { commitment: 'confirmed' })
|
578
|
-
.subscribe({ abortSignal });
|
579
|
-
|
580
|
-
// Consume messages.
|
581
|
-
let previousOwner = null;
|
582
|
-
for await (const notification of accountNotifications) {
|
583
|
-
const {
|
584
|
-
value: { owner },
|
585
|
-
} = notification;
|
586
|
-
// Check the owner to see if it has changed
|
587
|
-
if (previousOwner && owner !== previousOwner) {
|
588
|
-
// If so, abort the subscription
|
589
|
-
abortController.abort();
|
590
|
-
} else {
|
591
|
-
console.log(notification);
|
592
|
-
}
|
593
|
-
previousOwner = owner;
|
594
|
-
}
|
595
|
-
```
|
596
|
-
|
597
|
-
### Failed vs. Aborted Subscriptions
|
598
|
-
|
599
|
-
It is important to note that a subscription failure behaves differently from a subscription abort. A subscription failure occurs when the subscription goes down and will throw an error that can be intercepted in a `try/catch`. However, an aborted subscription will not throw an error, but will instead exit the `for await...of` loop.
|
600
|
-
|
601
|
-
```ts
|
602
|
-
try {
|
603
|
-
for await (const notification of notifications) {
|
604
|
-
// Consume messages.
|
605
|
-
}
|
606
|
-
// [ABORTED] Reaching this line means the subscription was aborted — i.e. unsubscribed.
|
607
|
-
} catch (e) {
|
608
|
-
// [FAILED] Reaching this line means the subscription went down.
|
609
|
-
// Retry it, then recover from potential missed messages.
|
610
|
-
} finally {
|
611
|
-
// [ABORTED or FAILED] Whether the subscription failed or was aborted, you can run cleanup code here.
|
612
|
-
}
|
613
|
-
```
|
614
|
-
|
615
|
-
### Message Gap Recovery
|
616
|
-
|
617
|
-
One of the most crucial aspects of any subscription API is managing potential missed messages. Missing messages, such as account state updates, could be catastrophic for an application. That’s why the new library provides native support for recovering missed messages using the `AsyncIterator`.
|
618
|
-
|
619
|
-
When a connection fails unexpectedly, any messages you miss while disconnected can result in your UI falling behind or becoming corrupt. Because subscription failure is now made explicit in the new API, you can implement ‘catch-up’ logic after re-establishing the subscription.
|
620
|
-
|
621
|
-
Here’s an example of such logic:
|
622
|
-
|
623
|
-
```ts
|
624
|
-
try {
|
625
|
-
for await (const notif of accountNotifications) {
|
626
|
-
updateAccountBalance(notif.lamports);
|
627
|
-
}
|
628
|
-
} catch (e) {
|
629
|
-
// The subscription failed.
|
630
|
-
// First, re-establish the subscription.
|
631
|
-
await setupAccountBalanceSubscription(address);
|
632
|
-
// Then make a one-shot request to 'catch up' on any missed balance changes.
|
633
|
-
const { value: lamports } = await rpc.getBalance(address).send();
|
634
|
-
updateAccountBalance(lamports);
|
635
|
-
}
|
636
|
-
```
|
637
|
-
|
638
|
-
### Using Custom RPC Subscriptions Transports
|
639
|
-
|
640
|
-
The `createSolanaRpcSubscriptions` function communicates with the RPC server using a default WebSocket transport that should satisfy most use cases. However, you may here as well provide your own transport or decorate existing ones to communicate with RPC servers in any way you see fit. In the example below, we explicitly create a WebSocket transport and use it to create a new RPC Subscriptions client via the `createSolanaRpcSubscriptionsFromTransport` function.
|
641
|
-
|
642
|
-
```ts
|
643
|
-
import { createDefaultRpcSubscriptionsTransport, createSolanaRpcSubscriptionsFromTransport } from '@solana/web3.js';
|
644
|
-
|
645
|
-
// Create a WebSocket transport or any custom transport of your choice.
|
646
|
-
const transport = createDefaultRpcSubscriptionsTransport({ url: 'ws://127.0.0.1:8900' });
|
647
|
-
|
648
|
-
// Create an RPC client using that transport.
|
649
|
-
const rpcSubscriptions = createSolanaRpcSubscriptionsFromTransport(transport);
|
650
|
-
// ^? RpcSubscriptions<SolanaRpcSubscriptionsApi>
|
651
|
-
```
|
652
|
-
|
653
|
-
### Augmenting/Constraining the RPC Subscriptions API
|
654
|
-
|
655
|
-
Using the `createSolanaRpcSubscriptions` or `createSolanaRpcSubscriptionsFromTransport` functions, we always get the same RPC Subscriptions API, including all Solana RPC stable subscriptions. However, since the RPC Subscriptions API is described using types only, it is possible to constrain the API to a specific set of subscriptions or even add your own custom subscriptions.
|
656
|
-
|
657
|
-
#### Constraining by Cluster
|
658
|
-
|
659
|
-
If you're using a specific cluster, you may wrap your RPC URL inside a helper function like `mainnet` or `devnet` to inject that information into the RPC type system.
|
660
|
-
|
661
|
-
```ts
|
662
|
-
import { createSolanaRpcSubscriptions, mainnet, devnet } from '@solana/web3.js';
|
663
|
-
|
664
|
-
const mainnetRpc = createSolanaRpcSubscriptions(mainnet('https://api.mainnet-beta.solana.com'));
|
665
|
-
// ^? RpcSubscriptionsMainnet<SolanaRpcSubscriptionsApi>
|
666
|
-
|
667
|
-
const devnetRpc = createSolanaRpcSubscriptions(devnet('https://api.devnet.solana.com'));
|
668
|
-
// ^? RpcSubscriptionsDevnet<SolanaRpcSubscriptionsApi>
|
669
|
-
```
|
670
|
-
|
671
|
-
#### Including Unstable Subscriptions
|
672
|
-
|
673
|
-
If your app needs access to [unstable RPC Subscriptions](https://docs.solana.com/api/websocket#blocksubscribe) — e.g. `BlockNotificationsApi` or `SlotsUpdatesNotificationsApi` — and your RPC server supports them, you may use the `createSolanaRpcSubscriptions_UNSTABLE` and `createSolanaRpcSubscriptionsFromTransport_UNSTABLE` functions to create an RPC Subscriptions client that includes those subscriptions.
|
674
|
-
|
675
|
-
```ts
|
676
|
-
import {
|
677
|
-
createSolanaRpcSubscriptions_UNSTABLE,
|
678
|
-
createSolanaRpcSubscriptionsFromTransport_UNSTABLE,
|
679
|
-
} from '@solana/web3.js';
|
680
|
-
|
681
|
-
// Using the default WebSocket transport.
|
682
|
-
const rpcSubscriptions = createSolanaRpcSubscriptions_UNSTABLE('ws://127.0.0.1:8900');
|
683
|
-
// ^? RpcSubscriptions<SolanaRpcSubscriptionsApi & SolanaRpcSubscriptionsApiUnstable>
|
684
|
-
|
685
|
-
// Using a custom transport.
|
686
|
-
const transport = createDefaultRpcSubscriptionsTransport({ url: 'ws://127.0.0.1:8900' });
|
687
|
-
const rpcSubscriptions = createSolanaRpcSubscriptionsFromTransport_UNSTABLE(transport);
|
688
|
-
// ^? RpcSubscriptions<SolanaRpcSubscriptionsApi & SolanaRpcSubscriptionsApiUnstable>
|
689
|
-
```
|
690
|
-
|
691
|
-
#### Cherry-Picking API Methods
|
692
|
-
|
693
|
-
You may constrain the scope of the Subscription API even further so you are left only with the subscriptions you need. The simplest way to do this is to cast the created RPC client to a type that only includes the methods you need.
|
694
|
-
|
695
|
-
```ts
|
696
|
-
import {
|
697
|
-
createSolanaRpcSubscriptions,
|
698
|
-
type RpcSubscriptions,
|
699
|
-
type AccountNotificationsApi,
|
700
|
-
type SlotNotificationsApi,
|
701
|
-
} from '@solana/web3.js';
|
702
|
-
|
703
|
-
const rpc = createSolanaRpcSubscriptions('ws://127.0.0.1:8900') as RpcSubscriptions<
|
704
|
-
AccountNotificationsApi & SlotNotificationsApi
|
705
|
-
>;
|
706
|
-
```
|
707
|
-
|
708
|
-
Alternatively, you may explicitly create the RPC Subscriptions API using the `createSolanaRpcSubscriptionsApi` function. You will then need to create your own transport explicitly and bind the two together using the `createSubscriptionRpc` function.
|
709
|
-
|
710
|
-
```ts
|
711
|
-
import {
|
712
|
-
createDefaultRpcSubscriptionsTransport,
|
713
|
-
createSubscriptionRpc,
|
714
|
-
createSolanaRpcSubscriptionsApi,
|
715
|
-
DEFAULT_RPC_CONFIG,
|
716
|
-
type AccountNotificationsApi,
|
717
|
-
type SlotNotificationsApi,
|
718
|
-
} from '@solana/web3.js';
|
719
|
-
|
720
|
-
const api = createSolanaRpcSubscriptionsApi<AccountNotificationsApi & SlotNotificationsApi>(DEFAULT_RPC_CONFIG);
|
721
|
-
const transport = createDefaultRpcSubscriptionsTransport({ url: 'ws://127.0.0.1:8900' });
|
722
|
-
const rpcSubscriptions = createSubscriptionRpc({ api, transport });
|
723
|
-
```
|
724
|
-
|
725
|
-
Note that the `createSolanaRpcSubscriptionsApi` function is a wrapper on top of the `createRpcSubscriptionsApi` function which adds some Solana-specific transformers such as setting a default commitment on all methods or throwing an error when an integer overflow is detected.
|
726
|
-
|
727
|
-
## Keys
|
728
|
-
|
729
|
-
The new library takes a brand-new approach to Solana key pairs and addresses, which will feel quite different from the classes `PublicKey` and `Keypair` from version 1.x.
|
730
|
-
|
731
|
-
### Web Crypto API
|
732
|
-
|
733
|
-
All key operations now use the native Ed25519 implementation in JavaScript’s Web Crypto API.
|
734
|
-
|
735
|
-
The API itself is designed to be a more reliably secure way to manage highly sensitive secret key information, but **developers should still use extreme caution when dealing with secret key bytes in their applications**.
|
736
|
-
|
737
|
-
One thing to note is that many operations from Web Crypto – such as importing, generating, signing, and verifying are now **asynchronous**.
|
738
|
-
|
739
|
-
Here’s an example of generating a `CryptoKeyPair` using the Web Crypto API and signing a message:
|
740
|
-
|
741
|
-
```ts
|
742
|
-
import { generateKeyPair, signBytes, verifySignature } from '@solana/web3.js';
|
743
|
-
|
744
|
-
const keyPair: CryptoKeyPair = await generateKeyPair();
|
745
|
-
|
746
|
-
const message = new Uint8Array(8).fill(0);
|
747
|
-
|
748
|
-
const signedMessage = await signBytes(keyPair.privateKey, message);
|
749
|
-
// ^? Signature
|
750
|
-
|
751
|
-
const verified = await verifySignature(keyPair.publicKey, signedMessage, message);
|
752
|
-
```
|
753
|
-
|
754
|
-
### Web Crypto Polyfill
|
755
|
-
|
756
|
-
Wherever Ed25519 is not supported, we offer a polyfill for Web Crypto’s Ed25519 API.
|
757
|
-
|
758
|
-
This polyfill can be found at `@solana/webcrypto-ed25519-polyfill` and mimics the functionality of the Web Crypto API for Ed25519 key pairs using the same userspace implementation we used in web3.js 1.x. It does not polyfill other algorithms.
|
759
|
-
|
760
|
-
Determine if your target runtime supports Ed25519, and install the polyfill if it does not:
|
761
|
-
|
762
|
-
```ts
|
763
|
-
import install from '@solana/webcrypto-ed25519-polyfill';
|
764
|
-
import { generateKeyPair, signBytes, verifySignature } from '@solana/web3.js';
|
765
|
-
|
766
|
-
install();
|
767
|
-
const keyPair: CryptoKeyPair = await generateKeyPair();
|
768
|
-
|
769
|
-
/* Remaining logic */
|
770
|
-
```
|
771
|
-
|
772
|
-
You can see where Ed25519 is currently supported in [this GitHub issue](https://github.com/WICG/webcrypto-secure-curves/issues/20) on the Web Crypto repository. Consider sniffing the user-agent when deciding whether or not to deliver the polyfill to browsers.
|
773
|
-
|
774
|
-
Operations on `CryptoKey` objects using the Web Crypto API _or_ the polyfill are mostly handled by the `@solana/keys` package.
|
775
|
-
|
776
|
-
### String Addresses
|
777
|
-
|
778
|
-
All addresses are now JavaScript strings. They are represented by the opaque type `Address`, which describes exactly what a Solana address actually is.
|
779
|
-
|
780
|
-
Consequently, that means no more `PublicKey`.
|
781
|
-
|
782
|
-
Here’s what they look like in development:
|
783
|
-
|
784
|
-
```ts
|
785
|
-
import { Address, address, getAddressFromPublicKey, generateKeyPair } from '@solana/web3.js';
|
786
|
-
|
787
|
-
// Coerce a string to an `Address`
|
788
|
-
const myOtherAddress = address('AxZfZWeqztBCL37Mkjkd4b8Hf6J13WCcfozrBY6vZzv3');
|
789
|
-
|
790
|
-
// Typecast it instead
|
791
|
-
const myAddress =
|
792
|
-
'AxZfZWeqztBCL37Mkjkd4b8Hf6J13WCcfozrBY6vZzv3' as Address<'AxZfZWeqztBCL37Mkjkd4b8Hf6J13WCcfozrBY6vZzv3'>;
|
793
|
-
|
794
|
-
// From CryptoKey
|
795
|
-
const keyPair = await generateKeyPair();
|
796
|
-
const myPublicKeyAsAddress = await getAddressFromPublicKey(keyPair.publicKey);
|
797
|
-
```
|
798
|
-
|
799
|
-
Some tooling for working with base58-encoded addresses can be found in the `@solana/addresses` package.
|
800
|
-
|
801
|
-
## Transactions
|
802
|
-
|
803
|
-
### Creating Transaction Messages
|
21
|
+
In addition to reexporting functions from packages in the `@solana/*` namespace, this package offers additional helpers for building Solana applications, with sensible defaults.
|
804
22
|
|
805
|
-
|
23
|
+
### `airdropFactory({rpc, rpcSubscriptions})`
|
806
24
|
|
807
|
-
|
808
|
-
|
809
|
-
Address lookups are now completely described inside transaction message instructions, so you don’t have to materialize `addressTableLookups` anymore.
|
810
|
-
|
811
|
-
Here’s a simple example of creating a transaction message – notice how its type is refined at each step of the process:
|
25
|
+
Returns a function that you can call to airdrop a certain amount of `Lamports` to a Solana address.
|
812
26
|
|
813
27
|
```ts
|
814
28
|
import {
|
815
29
|
address,
|
816
|
-
|
817
|
-
|
818
|
-
|
819
|
-
|
30
|
+
airdropFactory,
|
31
|
+
createSolanaRpc,
|
32
|
+
createSolanaRpcSubscriptions,
|
33
|
+
devnet,
|
34
|
+
lamports,
|
820
35
|
} from '@solana/web3.js';
|
821
36
|
|
822
|
-
const
|
823
|
-
|
824
|
-
lastValidBlockHeight: 196055492n,
|
825
|
-
};
|
826
|
-
const feePayer = address('AxZfZWeqztBCL37Mkjkd4b8Hf6J13WCcfozrBY6vZzv3');
|
827
|
-
|
828
|
-
// Create a new transaction message
|
829
|
-
const transactionMessage = createTransactionMessage({ version: 0 });
|
830
|
-
// ^? V0TransactionMessage
|
37
|
+
const rpc = createSolanaRpc(devnet('http://127.0.0.1:8899'));
|
38
|
+
const rpcSubscriptions = createSolanaRpcSubscriptions(devnet('ws://127.0.0.1:8900'));
|
831
39
|
|
832
|
-
|
833
|
-
const transactionMessageWithFeePayer = setTransactionMessageFeePayer(feePayer, transactionMessage);
|
834
|
-
// ^? V0TransactionMessage & ITransactionMessageWithFeePayer
|
40
|
+
const airdrop = airdropFactory({ rpc, rpcSubscriptions });
|
835
41
|
|
836
|
-
|
837
|
-
|
838
|
-
|
839
|
-
|
840
|
-
);
|
42
|
+
await airdrop({
|
43
|
+
commitment: 'confirmed',
|
44
|
+
recipientAddress: address('FnHyam9w4NZoWR6mKN1CuGBritdsEWZQa4Z4oawLZGxa'),
|
45
|
+
lamports: lamports(10_000_000n),
|
46
|
+
});
|
841
47
|
```
|
842
48
|
|
843
|
-
|
844
|
-
|
845
|
-
Transaction message objects are also **frozen by these functions** to prevent them from being mutated in place.
|
49
|
+
> [!NOTE] This only works on test clusters.
|
846
50
|
|
847
|
-
###
|
51
|
+
### `decodeTransactionMessage(compiledTransactionMessage, rpc, config)`
|
848
52
|
|
849
|
-
|
53
|
+
Returns a `TransactionMessage` from a `CompiledTransactionMessage`. If any of the accounts in the compiled message require an address lookup table to find their address, this function will use the supplied RPC instance to fetch the contents of the address lookup table from the network.
|
850
54
|
|
851
|
-
|
852
|
-
const feePayer = address('AxZfZWeqztBCL37Mkjkd4b8Hf6J13WCcfozrBY6vZzv3');
|
853
|
-
const signer = await generateKeyPair();
|
854
|
-
|
855
|
-
const transactionMessage = createTransactionMessage({ version: 'legacy' });
|
856
|
-
const transactionMessageWithFeePayer = setTransactionMessageFeePayer(feePayer, transactionMessage);
|
55
|
+
### `fetchLookupTables(lookupTableAddresses, rpc, config)`
|
857
56
|
|
858
|
-
|
859
|
-
const signedTransaction = await signTransaction([signer], transactionMessageWithFeePayer);
|
860
|
-
// => "Property 'lifetimeConstraint' is missing in type"
|
861
|
-
```
|
57
|
+
Given a list of addresses belonging to address lookup tables, returns a map of lookup table addresses to an ordered array of the addresses they contain.
|
862
58
|
|
863
|
-
###
|
59
|
+
### `getComputeUnitEstimateForTransactionMessageFactory({rpc})`
|
864
60
|
|
865
61
|
Correctly budgeting a compute unit limit for your transaction message can increase the probability that your transaction will be accepted for processing. If you don't declare a compute unit limit on your transaction, validators will assume an upper limit of 200K compute units (CU) per instruction.
|
866
62
|
|
@@ -900,532 +96,75 @@ const transactionMessageWithComputeUnitLimit = prependTransactionMessageInstruct
|
|
900
96
|
> [!NOTE]
|
901
97
|
> If you are preparing an _unsigned_ transaction, destined to be signed and submitted to the network by a wallet, you might like to leave it up to the wallet to determine the compute unit limit. Consider that the wallet might have a more global view of how many compute units certain types of transactions consume, and might be able to make better estimates of an appropriate compute unit budget.
|
902
98
|
|
903
|
-
###
|
99
|
+
### `sendAndConfirmDurableNonceTransactionFactory({rpc, rpcSubscriptions})`
|
904
100
|
|
905
|
-
|
101
|
+
Returns a function that you can call to send a nonce-based transaction to the network and to wait until it has been confirmed.
|
906
102
|
|
907
103
|
```ts
|
908
|
-
import { pipe } from '@solana/functional';
|
909
104
|
import {
|
910
|
-
|
911
|
-
|
912
|
-
|
913
|
-
|
914
|
-
Blockhash,
|
105
|
+
isSolanaError,
|
106
|
+
sendAndConfirmDurableNonceTransactionFactory,
|
107
|
+
SOLANA_ERROR__INVALID_NONCE,
|
108
|
+
SOLANA_ERROR__NONCE_ACCOUNT_NOT_FOUND,
|
915
109
|
} from '@solana/web3.js';
|
916
110
|
|
917
|
-
|
918
|
-
const transactionMessage = pipe(
|
919
|
-
createTransactionMessage({ version: 0 }),
|
920
|
-
tx => setTransactionMessageFeePayer(feePayer, tx),
|
921
|
-
tx => setTransactionMessageLifetimeUsingBlockhash(recentBlockhash, tx),
|
922
|
-
);
|
923
|
-
```
|
924
|
-
|
925
|
-
Note that `pipe(..)` is general-purpose, so it can be used to pipeline any functional transforms.
|
926
|
-
|
927
|
-
## Codecs
|
928
|
-
|
929
|
-
We have taken steps to make it easier to write data (de)serializers, especially as they pertain to Rust datatypes and byte buffers.
|
930
|
-
|
931
|
-
Solana’s codecs libraries are broken up into modular components so you only need to import the ones you need. They are:
|
932
|
-
|
933
|
-
- `@solana/codecs-core`: The core codecs library for working with codecs serializers and creating custom ones
|
934
|
-
- `@solana/codecs-numbers`: Used for serialization of numbers (little-endian and big-endian bytes, etc.)
|
935
|
-
- `@solana/codecs-strings`: Used for serialization of strings
|
936
|
-
- `@solana/codecs-data-structures`: Codecs and serializers for structs
|
937
|
-
- `@solana/options`: Designed to build codecs and serializers for types that mimic Rust’s enums, which can include embedded data within their variants such as values, tuples, and structs
|
938
|
-
|
939
|
-
These packages are included in the main `@solana/web3.js` library but you may also import them from `@solana/codecs` if you only need the codecs.
|
940
|
-
|
941
|
-
Here’s an example of encoding and decoding a custom struct with some strings and numbers:
|
942
|
-
|
943
|
-
```ts
|
944
|
-
import { addCodecSizePrefix } from '@solana/codecs-core';
|
945
|
-
import { getStructCodec } from '@solana/codecs-data-structures';
|
946
|
-
import { getU32Codec, getU64Codec, getU8Codec } from '@solana/codecs-numbers';
|
947
|
-
import { getUtf8Codec } from '@solana/codecs-strings';
|
948
|
-
|
949
|
-
// Equivalent in Rust:
|
950
|
-
// struct {
|
951
|
-
// amount: u64,
|
952
|
-
// decimals: u8,
|
953
|
-
// name: String,
|
954
|
-
// }
|
955
|
-
const structCodec = getStructCodec([
|
956
|
-
['amount', getU64Codec()],
|
957
|
-
['decimals', getU8Codec()],
|
958
|
-
['name', addCodecSizePrefix(getUtf8Codec(), getU32Codec())],
|
959
|
-
]);
|
960
|
-
|
961
|
-
const myToken = {
|
962
|
-
amount: 1000000000000000n, // `bigint` or `number` is supported
|
963
|
-
decimals: 2,
|
964
|
-
name: 'My Token',
|
965
|
-
};
|
966
|
-
|
967
|
-
const myEncodedToken: Uint8Array = structCodec.encode(myToken);
|
968
|
-
const myDecodedToken = structCodec.decode(myEncodedToken);
|
969
|
-
|
970
|
-
myDecodedToken satisfies {
|
971
|
-
amount: bigint;
|
972
|
-
decimals: number;
|
973
|
-
name: string;
|
974
|
-
};
|
975
|
-
```
|
976
|
-
|
977
|
-
You may only need to encode or decode data, but not both. Importing one or the other allows your optimizing compiler to tree-shake the other implementation away:
|
978
|
-
|
979
|
-
```ts
|
980
|
-
import { Codec, combineCodec, Decoder, Encoder, addDecoderSizePrefix, addEncoderSizePrefix } from '@solana/codecs-core';
|
981
|
-
import { getStructDecoder, getStructEncoder } from '@solana/codecs-data-structures';
|
982
|
-
import {
|
983
|
-
getU8Decoder,
|
984
|
-
getU8Encoder,
|
985
|
-
getU32Decoder,
|
986
|
-
getU32Encoder,
|
987
|
-
getU64Decoder,
|
988
|
-
getU64Encoder,
|
989
|
-
} from '@solana/codecs-numbers';
|
990
|
-
import { getUtf8Decoder, getUtf8Encoder } from '@solana/codecs-strings';
|
991
|
-
|
992
|
-
export type MyToken = {
|
993
|
-
amount: bigint;
|
994
|
-
decimals: number;
|
995
|
-
name: string;
|
996
|
-
};
|
997
|
-
|
998
|
-
export type MyTokenArgs = {
|
999
|
-
amount: number | bigint;
|
1000
|
-
decimals: number;
|
1001
|
-
name: string;
|
1002
|
-
};
|
1003
|
-
|
1004
|
-
export const getMyTokenEncoder = (): Encoder<MyTokenArgs> =>
|
1005
|
-
getStructEncoder([
|
1006
|
-
['amount', getU64Encoder()],
|
1007
|
-
['decimals', getU8Encoder()],
|
1008
|
-
['name', addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())],
|
1009
|
-
]);
|
1010
|
-
|
1011
|
-
export const getMyTokenDecoder = (): Decoder<MyToken> =>
|
1012
|
-
getStructDecoder([
|
1013
|
-
['amount', getU64Decoder()],
|
1014
|
-
['decimals', getU8Decoder()],
|
1015
|
-
['name', addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())],
|
1016
|
-
]);
|
1017
|
-
|
1018
|
-
export const getMyTokenCodec = (): Codec<MyTokenArgs, MyToken> =>
|
1019
|
-
combineCodec(getMyTokenEncoder(), getMyTokenDecoder());
|
1020
|
-
```
|
1021
|
-
|
1022
|
-
You can read me about codecs in [the official Codec documentation](https://github.com/solana-labs/solana-web3.js/blob/master/packages/codecs/README.md).
|
1023
|
-
|
1024
|
-
## Type-Safety
|
1025
|
-
|
1026
|
-
The new library makes use of some advanced TypeScript features, including generic types, conditional types, `Parameters<..>`, `ReturnType<..>` and more.
|
1027
|
-
|
1028
|
-
We’ve described the RPC API in detail so that TypeScript can determine the _exact_ type of the result you will receive from the server given a particular input. Change the type of the input, and you will see the return type reflect that change.
|
1029
|
-
|
1030
|
-
### RPC Types
|
1031
|
-
|
1032
|
-
The RPC methods – both HTTP and subscriptions – are built with multiple overloads and conditional types. The expected HTTP response payload or subscription message format will be reflected in the return type of the function you’re working with when you provide the inputs in your code.
|
1033
|
-
|
1034
|
-
Here’s an example of this in action:
|
1035
|
-
|
1036
|
-
```ts
|
1037
|
-
// Provide one set of parameters, get a certain type
|
1038
|
-
// These parameters resolve to return type:
|
1039
|
-
// {
|
1040
|
-
// blockhash: Blockhash;
|
1041
|
-
// blockHeight: bigint;
|
1042
|
-
// blockTime: UnixTimestamp;
|
1043
|
-
// parentSlot: bigint;
|
1044
|
-
// previousBlockhash: Blockhash;
|
1045
|
-
// }
|
1046
|
-
const blockResponse = await rpc
|
1047
|
-
.getBlock(0n, {
|
1048
|
-
rewards: false,
|
1049
|
-
transactionDetails: 'none',
|
1050
|
-
})
|
1051
|
-
.send();
|
1052
|
-
|
1053
|
-
// Switch `rewards` to `true`, get `rewards` in the return type
|
1054
|
-
// {
|
1055
|
-
// /* ... Previous response */
|
1056
|
-
// rewards: Reward[];
|
1057
|
-
// }
|
1058
|
-
const blockWithRewardsResponse = await rpc
|
1059
|
-
.getBlock(0n, {
|
1060
|
-
rewards: true,
|
1061
|
-
transactionDetails: 'none',
|
1062
|
-
})
|
1063
|
-
.send();
|
1064
|
-
|
1065
|
-
// Switch `transactionDetails` to `full`, get `transactions` in the return type
|
1066
|
-
// {
|
1067
|
-
// /* ... Previous response */
|
1068
|
-
// transactions: TransactionResponse[];
|
1069
|
-
// }
|
1070
|
-
const blockWithRewardsAndTransactionsResponse = await rpc
|
1071
|
-
.getBlock(0n, {
|
1072
|
-
rewards: true,
|
1073
|
-
transactionDetails: 'full',
|
1074
|
-
})
|
1075
|
-
.send();
|
1076
|
-
```
|
1077
|
-
|
1078
|
-
### Catching Compile-Time Bugs with TypeScript
|
1079
|
-
|
1080
|
-
As previously mentioned, the type coverage in version 2.0 allows developers to catch common bugs at compile time, rather than runtime.
|
1081
|
-
|
1082
|
-
In the example below, a transaction message is created and then attempted to be signed without setting the fee payer. This would result in a runtime error from the RPC, but instead you will see a type error from TypeScript as you type:
|
1083
|
-
|
1084
|
-
```ts
|
1085
|
-
const transactionMessage = pipe(createTransactionMessage({ version: 0 }), tx =>
|
1086
|
-
setTransactionMessageLifetimeUsingBlockhash(recentBlockhash, tx),
|
1087
|
-
);
|
1088
|
-
const signedTransaction = await signTransaction([keyPair], transactionMessage); // ERROR: Property 'feePayer' is missing in type
|
1089
|
-
```
|
1090
|
-
|
1091
|
-
Consider another example where a developer is attempting to send a transaction that has not been fully signed. Again, the TypeScript compiler will throw a type error:
|
1092
|
-
|
1093
|
-
```ts
|
1094
|
-
const transactionMessage = pipe(
|
1095
|
-
createTransactionMessage({ version: 0 }),
|
1096
|
-
tx => setTransactionMessageFeePayer(feePayerAddress, tx),
|
1097
|
-
tx => setTransactionMessageLifetimeUsingBlockhash(recentBlockhash, tx),
|
1098
|
-
);
|
1099
|
-
|
1100
|
-
const signedTransaction = await signTransaction([], transactionMessage);
|
1101
|
-
|
1102
|
-
// Asserts the transaction is a `FullySignedTransaction`
|
1103
|
-
// Throws an error if any signatures are missing!
|
1104
|
-
assertTransactionIsFullySigned(signedTransaction);
|
1105
|
-
|
1106
|
-
await sendAndConfirmTransaction(signedTransaction);
|
1107
|
-
```
|
1108
|
-
|
1109
|
-
Are you building a nonce transaction and forgot to make `AdvanceNonce` the first instruction? That’s a type error:
|
1110
|
-
|
1111
|
-
```ts
|
1112
|
-
const feePayer = await generateKeyPair();
|
1113
|
-
const feePayerAddress = await getAddressFromPublicKey(feePayer.publicKey);
|
1114
|
-
|
1115
|
-
const notNonceTransactionMessage = pipe(createTransactionMessage({ version: 0 }), tx =>
|
1116
|
-
setTransactionMessageFeePayer(feePayerAddress, tx),
|
1117
|
-
);
|
1118
|
-
|
1119
|
-
notNonceTransactionMessage satisfies TransactionMessageWithDurableNonceLifetime;
|
1120
|
-
// => Property 'lifetimeConstraint' is missing in type
|
1121
|
-
|
1122
|
-
const nonceConfig = {
|
1123
|
-
nonce: 'nonce' as Nonce,
|
1124
|
-
nonceAccountAddress: address('5tLU66bxQ35so2bReGcyf3GfMMAAauZdNA1N4uRnKQu4'),
|
1125
|
-
nonceAuthorityAddress: address('GDhj8paPg8woUzp9n8fj7eAMocN5P7Ej3A7T9F5gotTX'),
|
1126
|
-
};
|
1127
|
-
|
1128
|
-
const stillNotNonceTransactionMessage = {
|
1129
|
-
lifetimeConstraint: nonceConfig,
|
1130
|
-
...notNonceTransactionMessage,
|
1131
|
-
};
|
1132
|
-
|
1133
|
-
stillNotNonceTransactionMessage satisfies TransactionMessageWithDurableNonceLifetime;
|
1134
|
-
// => 'readonly IInstruction<string>[]' is not assignable to type 'readonly [AdvanceNonceAccountInstruction<string, string>, ...IInstruction<string>[]]'
|
1135
|
-
|
1136
|
-
const validNonceTransactionMessage = pipe(
|
1137
|
-
createTransactionMessage({ version: 0 }),
|
1138
|
-
tx => setTransactionMessageFeePayer(feePayerAddress, tx),
|
1139
|
-
tx => setTransactionMessageLifetimeUsingDurableNonce(nonceConfig, tx), // Adds the instruction!
|
1140
|
-
);
|
1141
|
-
|
1142
|
-
validNonceTransactionMessage satisfies TransactionMessageWithDurableNonceLifetime; // OK
|
1143
|
-
```
|
1144
|
-
|
1145
|
-
The library’s type-checking can even catch you using lamports instead of SOL for a value:
|
1146
|
-
|
1147
|
-
```ts
|
1148
|
-
const airdropAmount = 1n; // SOL
|
1149
|
-
const signature = rpc.requestAirdrop(myAddress, airdropAmount).send();
|
1150
|
-
```
|
1151
|
-
|
1152
|
-
It will force you to cast the numerical value for your airdrop (or transfer, etc.) amount using `lamports()`, which should be a good reminder!
|
1153
|
-
|
1154
|
-
```ts
|
1155
|
-
const airdropAmount = lamports(1000000000n);
|
1156
|
-
const signature = rpc.requestAirdrop(myAddress, airdropAmount).send();
|
1157
|
-
```
|
1158
|
-
|
1159
|
-
## Compatibility Layer
|
1160
|
-
|
1161
|
-
You will have noticed by now that web3.js is a complete and total breaking change from the 1.x line. We want to provide you with a strategy for interacting with 1.x APIs while building your application using 2.0. You need a tool for commuting between 1.x and 2.0 data types.
|
1162
|
-
|
1163
|
-
The `@solana/compat` library allows for interoperability between functions and class objects from the legacy library - such as `VersionedTransaction`, `PublicKey`, and `Keypair` - and functions and types of the new library - such as `Address`, `Transaction`, and `CryptoKeyPair`.
|
1164
|
-
|
1165
|
-
Here’s how you can use `@solana/compat` to convert from a legacy `PublicKey` to an `Address`:
|
1166
|
-
|
1167
|
-
```ts
|
1168
|
-
import { fromLegacyPublicKey } from '@solana/compat';
|
1169
|
-
|
1170
|
-
const publicKey = new PublicKey('B3piXWBQLLRuk56XG5VihxR4oe2PSsDM8nTF6s1DeVF5');
|
1171
|
-
const address: Address = fromLegacyPublicKey(publicKey);
|
1172
|
-
```
|
1173
|
-
|
1174
|
-
Here’s how to convert from a legacy `Keypair` to a `CryptoKeyPair`:
|
1175
|
-
|
1176
|
-
```ts
|
1177
|
-
import { fromLegacyKeypair } from '@solana/compat';
|
1178
|
-
|
1179
|
-
const keypairLegacy = Keypair.generate();
|
1180
|
-
const cryptoKeyPair: CryptoKeyPair = fromLegacyKeypair(keypair);
|
1181
|
-
```
|
1182
|
-
|
1183
|
-
Here’s how to convert legacy transaction objects to the new library’s transaction types:
|
1184
|
-
|
1185
|
-
```ts
|
1186
|
-
// Note that you can only convert `VersionedTransaction` objects
|
1187
|
-
const modernTransaction = fromVersionedTransaction(classicTransaction);
|
1188
|
-
```
|
1189
|
-
|
1190
|
-
To see more conversions supported by `@solana/compat`, you can check out the package’s [README on GitHub](https://github.com/solana-labs/solana-web3.js/blob/master/packages/compat/README.md).
|
1191
|
-
|
1192
|
-
## Program Clients
|
1193
|
-
|
1194
|
-
Writing JavaScript clients for on-chain programs has been done manually up until now. Without an IDL for some of the native programs, this process has been necessarily manual and has resulted in clients that lag behind the actual capabilities of the programs themselves.
|
1195
|
-
|
1196
|
-
We think that program clients should be _generated_ rather than written. Developers should be able to write Rust programs, compile the program code, and generate all of the JavaScript client-side code to interact with the program.
|
1197
|
-
|
1198
|
-
We use [Kinobi](https://github.com/metaplex-foundation/kinobi) to represent Solana programs and generate clients for them. This includes a JavaScript client compatible with this library. For instance, here is how you’d construct a transaction message composed of instructions from three different core programs.
|
1199
|
-
|
1200
|
-
```ts
|
1201
|
-
import { appendTransactionMessageInstructions, createTransactionMessage, pipe } from '@solana/web3.js';
|
1202
|
-
import { getAddMemoInstruction } from '@solana-program/memo';
|
1203
|
-
import { getSetComputeUnitLimitInstruction } from '@solana-program/compute-budget';
|
1204
|
-
import { getTransferSolInstruction } from '@solana-program/system';
|
1205
|
-
|
1206
|
-
const instructions = [
|
1207
|
-
getSetComputeUnitLimitInstruction({ units: 600_000 }),
|
1208
|
-
getTransferSolInstruction({ source, destination, amount: 1_000_000_000 }),
|
1209
|
-
getAddMemoInstruction({ memo: "I'm transferring some SOL!" }),
|
1210
|
-
];
|
1211
|
-
|
1212
|
-
// Creates a V0 transaction message with 3 instructions inside.
|
1213
|
-
const transactionMessage = pipe(createTransactionMessage({ version: 0 }), tx =>
|
1214
|
-
appendTransactionMessageInstructions(instructions, tx),
|
1215
|
-
);
|
1216
|
-
```
|
1217
|
-
|
1218
|
-
As you can see, each program now generates its own library allowing you to cherry-pick your dependencies.
|
1219
|
-
|
1220
|
-
Note that asynchronous versions may be available for some instructions which allows them to resolve more inputs on your behalf — such as PDA derivation. For instance, the `CreateLookupTable` instruction offers an asynchronous builder that derives the `address` account and the `bump` argument for us.
|
1221
|
-
|
1222
|
-
```ts
|
1223
|
-
const rpc = createSolanaRpc('http://127.0.0.1:8899');
|
1224
|
-
const [authority, recentSlot] = await Promise.all([
|
1225
|
-
generateKeyPairSigner(),
|
1226
|
-
rpc.getSlot({ commitment: 'finalized' }).send(),
|
1227
|
-
]);
|
1228
|
-
|
1229
|
-
const instruction = await getCreateLookupTableInstructionAsync({
|
1230
|
-
authority,
|
1231
|
-
recentSlot,
|
1232
|
-
});
|
1233
|
-
```
|
1234
|
-
|
1235
|
-
Alternatively, you may use the synchronous builder if you already have all the required inputs at hand.
|
1236
|
-
|
1237
|
-
```ts
|
1238
|
-
const [address, bump] = await findAddressLookupTablePda({
|
1239
|
-
authority: authority.address,
|
1240
|
-
recentSlot,
|
1241
|
-
});
|
111
|
+
const sendAndConfirmNonceTransaction = sendAndConfirmDurableNonceTransactionFactory({ rpc, rpcSubscriptions });
|
1242
112
|
|
1243
|
-
|
1244
|
-
|
1245
|
-
|
1246
|
-
|
1247
|
-
|
1248
|
-
|
113
|
+
try {
|
114
|
+
await sendAndConfirmNonceTransaction(transaction, { commitment: 'confirmed' });
|
115
|
+
} catch (e) {
|
116
|
+
if (isSolanaError(e, SOLANA_ERROR__NONCE_ACCOUNT_NOT_FOUND)) {
|
117
|
+
console.error(
|
118
|
+
'The lifetime specified by this transaction refers to a nonce account ' +
|
119
|
+
`\`${e.context.nonceAccountAddress}\` that does not exist`,
|
120
|
+
);
|
121
|
+
} else if (isSolanaError(e, SOLANA_ERROR__INVALID_NONCE)) {
|
122
|
+
console.error('This transaction depends on a nonce that is no longer valid');
|
123
|
+
} else {
|
124
|
+
throw e;
|
125
|
+
}
|
126
|
+
}
|
1249
127
|
```
|
1250
128
|
|
1251
|
-
|
129
|
+
### `sendAndConfirmTransactionFactory({rpc, rpcSubscriptions})`
|
1252
130
|
|
1253
|
-
-
|
1254
|
-
- Account types — e.g. `AddressLookupTable`.
|
1255
|
-
- Account codecs — e.g. `getAddressLookupTableAccountDataCodec`.
|
1256
|
-
- Account helpers — e.g. `fetchAddressLookupTable`.
|
1257
|
-
- PDA helpers — e.g. `findAddressLookupTablePda`, `fetchAddressLookupTableFromSeeds`.
|
1258
|
-
- Defined types and their codecs — e.g. `NonceState`, `getNonceStateCodec`.
|
1259
|
-
- Program helpers — e.g. `SYSTEM_PROGRAM_ADDRESS`, `SystemAccount` enum, `SystemAccount` enum, `identifySystemInstruction`.
|
1260
|
-
- And much more!
|
1261
|
-
|
1262
|
-
Here’s another example that fetches an `AddressLookupTable` PDA from its seeds.
|
131
|
+
Returns a function that you can call to send a blockhash-based transaction to the network and to wait until it has been confirmed.
|
1263
132
|
|
1264
133
|
```ts
|
1265
|
-
|
1266
|
-
authority: authority.address,
|
1267
|
-
recentSlot,
|
1268
|
-
});
|
1269
|
-
|
1270
|
-
account.address; // Address
|
1271
|
-
account.lamports; // LamportsUnsafeBeyond2Pow53Minus1
|
1272
|
-
account.data.addresses; // Address[]
|
1273
|
-
account.data.authority; // Some<Address>
|
1274
|
-
account.data.deactivationSlot; // Slot
|
1275
|
-
account.data.lastExtendedSlot; // Slot
|
1276
|
-
account.data.lastExtendedSlotStartIndex; // number
|
1277
|
-
```
|
134
|
+
import { isSolanaError, sendAndConfirmTransactionFactory, SOLANA_ERROR__BLOCK_HEIGHT_EXCEEDED } from '@solana/web3.js';
|
1278
135
|
|
1279
|
-
|
136
|
+
const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions });
|
1280
137
|
|
1281
|
-
|
1282
|
-
|
1283
|
-
|
1284
|
-
|
1285
|
-
|
1286
|
-
|
1287
|
-
|
1288
|
-
|
1289
|
-
We believe the whole ecosystem could benefit from generated program clients. That’s why we introduced a new NPM binary that allows you to create your Solana program — and generate clients for it — in no time. Simply run the following and follow the prompts to get started.
|
1290
|
-
|
1291
|
-
```sh
|
1292
|
-
pnpm create solana-program
|
1293
|
-
```
|
1294
|
-
|
1295
|
-
This [`create-solana-program`](https://github.com/solana-program/create-solana-program) installer will create a new repository including:
|
1296
|
-
|
1297
|
-
- An example program using the framework of your choice (Anchor coming soon).
|
1298
|
-
- Generated clients for any of the selected clients.
|
1299
|
-
- A set of scripts that allows you to:
|
1300
|
-
- Start a local validator including all programs and accounts you depend on.
|
1301
|
-
- Build, lint and test your programs.
|
1302
|
-
- Generate IDLs from your programs.
|
1303
|
-
- Generate clients from the generated IDLs.
|
1304
|
-
- Build and test each of your clients.
|
1305
|
-
- GitHub Actions pipelines to test your program, test your clients, and even manually publish new packages or crates for your clients. (Coming soon).
|
1306
|
-
|
1307
|
-
When selecting the JavaScript client, you will get a fully generated library compatible with the new web3.js much like the `@solana-program` packages showcased above.
|
1308
|
-
|
1309
|
-
## GraphQL
|
1310
|
-
|
1311
|
-
Though not directly related to web3.js, we wanted to hijack your attention to show you something else that we’re working on, of particular interest to frontend developers. It’s a new API for interacting with the RPC: a GraphQL API.
|
1312
|
-
|
1313
|
-
The `@solana/rpc-graphql` package can be used to make GraphQL queries to Solana RPC endpoints, using the same transports described above (including any customizations).
|
1314
|
-
|
1315
|
-
Here’s an example of retrieving account data with GraphQL:
|
1316
|
-
|
1317
|
-
```ts
|
1318
|
-
const source = `
|
1319
|
-
query myQuery($address: String!) {
|
1320
|
-
account(address: $address) {
|
1321
|
-
dataBase58: data(encoding: BASE_58)
|
1322
|
-
dataBase64: data(encoding: BASE_64)
|
1323
|
-
lamports
|
1324
|
-
}
|
138
|
+
try {
|
139
|
+
await sendAndConfirmTransaction(transaction, { commitment: 'confirmed' });
|
140
|
+
} catch (e) {
|
141
|
+
if (isSolanaError(e, SOLANA_ERROR__BLOCK_HEIGHT_EXCEEDED)) {
|
142
|
+
console.error('This transaction depends on a blockhash that has expired');
|
143
|
+
} else {
|
144
|
+
throw e;
|
1325
145
|
}
|
1326
|
-
|
1327
|
-
|
1328
|
-
const variableValues = {
|
1329
|
-
address: 'AyGCwnwxQMCqaU4ixReHt8h5W4dwmxU7eM3BEQBdWVca',
|
1330
|
-
};
|
1331
|
-
|
1332
|
-
const result = await rpcGraphQL.query(source, variableValues);
|
1333
|
-
|
1334
|
-
expect(result).toMatchObject({
|
1335
|
-
data: {
|
1336
|
-
account: {
|
1337
|
-
dataBase58: '2Uw1bpnsXxu3e',
|
1338
|
-
dataBase64: 'dGVzdCBkYXRh',
|
1339
|
-
lamports: 10290815n,
|
1340
|
-
},
|
1341
|
-
},
|
1342
|
-
});
|
146
|
+
}
|
1343
147
|
```
|
1344
148
|
|
1345
|
-
|
149
|
+
### `sendTransactionWithoutConfirmingFactory({rpc, rpcSubscriptions})`
|
1346
150
|
|
1347
|
-
|
151
|
+
Returns a function that you can call to send a transaction with any kind of lifetime to the network without waiting for it to be confirmed.
|
1348
152
|
|
1349
153
|
```ts
|
1350
|
-
|
1351
|
-
|
1352
|
-
|
1353
|
-
|
1354
|
-
owner {
|
1355
|
-
ownerProgram {
|
1356
|
-
lamports
|
1357
|
-
}
|
1358
|
-
}
|
1359
|
-
}
|
1360
|
-
}
|
1361
|
-
}
|
1362
|
-
`;
|
1363
|
-
|
1364
|
-
const result = await rpcGraphQL.query(source);
|
1365
|
-
|
1366
|
-
const sumOfAllLamportsOfOwnersOfOwnersOfTokenAccounts = result
|
1367
|
-
.map(o => o.account.owner.ownerProgram.lamports)
|
1368
|
-
.reduce((acc, lamports) => acc + lamports, 0);
|
1369
|
-
```
|
154
|
+
import {
|
155
|
+
sendTransactionWithoutConfirmingFactory,
|
156
|
+
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE,
|
157
|
+
} from '@solana/web3.js';
|
1370
158
|
|
1371
|
-
|
159
|
+
const sendTransaction = sendTransactionWithoutConfirmingFactory({ rpc });
|
1372
160
|
|
1373
|
-
|
1374
|
-
|
1375
|
-
|
1376
|
-
|
1377
|
-
|
1378
|
-
|
1379
|
-
|
1380
|
-
lamports
|
1381
|
-
programId
|
1382
|
-
space
|
1383
|
-
}
|
1384
|
-
}
|
1385
|
-
}
|
1386
|
-
}
|
161
|
+
try {
|
162
|
+
await sendTransaction(transaction, { commitment: 'confirmed' });
|
163
|
+
} catch (e) {
|
164
|
+
if (isSolanaError(e, SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE)) {
|
165
|
+
console.error('The transaction failed in simulation', e.cause);
|
166
|
+
} else {
|
167
|
+
throw e;
|
1387
168
|
}
|
1388
|
-
|
1389
|
-
|
1390
|
-
const variableValues = {
|
1391
|
-
signature: '63zkpxATgAwXRGFQZPDESTw2m4uZQ99sX338ibgKtTcgG6v34E3MSS3zckCwJHrimS71cvei6h1Bn1K1De53BNWC',
|
1392
|
-
commitment: 'confirmed',
|
1393
|
-
};
|
1394
|
-
|
1395
|
-
const result = await rpcGraphQL.query(source, variableValues);
|
1396
|
-
|
1397
|
-
expect(result).toMatchObject({
|
1398
|
-
data: {
|
1399
|
-
transaction: {
|
1400
|
-
message: {
|
1401
|
-
instructions: expect.arrayContaining([
|
1402
|
-
{
|
1403
|
-
lamports: expect.any(BigInt),
|
1404
|
-
programId: '11111111111111111111111111111111',
|
1405
|
-
space: expect.any(BigInt),
|
1406
|
-
},
|
1407
|
-
]),
|
1408
|
-
},
|
1409
|
-
},
|
1410
|
-
},
|
1411
|
-
});
|
169
|
+
}
|
1412
170
|
```
|
1413
|
-
|
1414
|
-
See more in the package’s [README on GitHub](https://github.com/solana-labs/solana-web3.js/tree/master/packages/rpc-graphql).
|
1415
|
-
|
1416
|
-
## Development
|
1417
|
-
|
1418
|
-
You can see all development of this library and associated GraphQL tooling in the web3.js repository on GitHub.
|
1419
|
-
|
1420
|
-
- https://github.com/solana-labs/solana-web3.js
|
1421
|
-
|
1422
|
-
You can follow along with program client generator development in the `@solana-program` org and the `@kinobi-so/kinobi` repository.
|
1423
|
-
|
1424
|
-
- https://github.com/solana-program/
|
1425
|
-
- https://github.com/kinobi-so/kinobi
|
1426
|
-
|
1427
|
-
Solana Labs develops these tools in public, as open source. We encourage any and all developers who would like to work on these tools to contribute to the codebase.
|
1428
|
-
|
1429
|
-
## Thank you
|
1430
|
-
|
1431
|
-
We’re grateful that you have read this far. If you are interested in migrating an existing application to the new web3.js to take advantage of some of the benefits we’ve demonstrated, we want to give you some direct support. Reach out to [@steveluscher](https://t.me/steveluscher/) on Telegram to start a conversation.
|