Skip to content

Commit

Permalink
Update web3.js README for new TransactionMessage API
Browse files Browse the repository at this point in the history
  • Loading branch information
steveluscher committed May 9, 2024
1 parent 0734032 commit 20e490a
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 129 deletions.
10 changes: 5 additions & 5 deletions packages/functional/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,14 @@ sum === 111; // true
A pipeline is one solution to performing consecutive operations on a value using functions, such as you would when building a transaction.

```ts
const transferTransaction = pipe(
const transferTransactionMessage = pipe(
// The result of the first expression...
createTransaction({ version: 0 }),
createTransactionMessage({ version: 0 }),
// ...gets passed as the sole argument to the next function in the pipeline.
tx => setTransactionFeePayer(myAddress, tx),
tx => setTransactionMessageFeePayer(myAddress, tx),
// The return value of that function gets passed to the next...
tx => setTransactionLifetimeUsingBlockhash(latestBlockhash, tx),
tx => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
// ...and so on.
tx => appendTransactionInstruction(createTransferInstruction(myAddress, toAddress, amountInLamports), tx),
tx => appendTransactionMessageInstruction(createTransferInstruction(myAddress, toAddress, amountInLamports), tx),
);
```
195 changes: 72 additions & 123 deletions packages/library/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,8 @@ The new library itself is comprised of several smaller, modular packages under t
- `@solana/rpc-subscriptions`: For subscribing to RPC notifications
- `@solana/signers`: For building message and/or transaction signer objects
- `@solana/sysvars`: For fetching and decoding sysvar accounts
- `@solana/transactions`: For building and transforming Solana transaction objects
- `@solana/transaction-messages`: For building and transforming Solana transaction message objects
- `@solana/transactions`: For compiling and signing transactions for submission to the network
- And many more!

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.
Expand Down Expand Up @@ -817,20 +818,22 @@ Some tooling for working with base58-encoded addresses can be found in the `@sol

## Transactions

Just like many other familiar aspects of the 1.0 library, transactions have received a makeover as well.
### Creating Transaction Messages

For starters, all transactions are now version-aware, so there’s no longer a need to juggle two different types of transactions (`Transaction` vs. `VersionedTransaction`).
Like many other familiar aspects of the 1.0 library, transactions have received a makeover.

Address lookups are now completely described inside transaction instructions, so you don’t have to materialize `addressTableLookups` from the transaction object anymore.
For starters, all transaction messages are now version-aware, so there’s no longer a need to juggle two different types (eg. `Transaction` vs. `VersionedTransaction`).

Here’s a simple example of creating a transaction – notice how the type of the transaction is refined at each step of the process:
Address lookups are now completely described inside transaction message instructions, so you don’t have to materialize `addressTableLookups` anymore.

Here’s a simple example of creating a transaction message – notice how its type is refined at each step of the process:

```ts
import {
address,
createTransaction,
setTransactionFeePayer,
setTransactionLifetimeUsingBlockhash,
createTransactionMessage,
setTransactionMessageFeePayer,
setTransactionMessageLifetimeUsingBlockhash,
Blockhash,
} from '@solana/web3.js';

Expand All @@ -840,110 +843,64 @@ const recentBlockhash = {
};
const feePayer = address('AxZfZWeqztBCL37Mkjkd4b8Hf6J13WCcfozrBY6vZzv3');

// Create a new transaction (legacy)
const transactionLegacy = createTransaction({ version: 'legacy' });
// ^? LegacyTransaction

const transactionWithFeePayerLegacy = setTransactionFeePayer(feePayer, transactionLegacy);
// ^? LegacyTransaction & ITransactionWithFeePayer

const transactionWithFeePayerAndLifetimeLegacy = setTransactionLifetimeUsingBlockhash(
recentBlockhash,
transactionWithFeePayerLegacy,
);
// ^? LegacyTransaction & ITransactionWithFeePayer & ITransactionWithBlockhash

// Create a new transaction (v0)
const transactionV0 = createTransaction({ version: 0 });
// ^? V0Transaction
// Create a new transaction message
const transactionMessage = createTransactionMessage({ version: 0 });
// ^? V0TransactionMessage

// Set the fee payer
const transactionWithFeePayerV0 = setTransactionFeePayer(feePayer, transactionV0);
// ^? V0Transaction & ITransactionWithFeePayer
const transactionMessageWithFeePayer = setTransactionMessageFeePayer(feePayer, transactionMessage);
// ^? V0TransactionMessage & ITransactionMessageWithFeePayer

const transactionWithFeePayerAndLifetimeV0 = setTransactionLifetimeUsingBlockhash(
const transactionMessageWithFeePayerAndLifetime = setTransactionMessageLifetimeUsingBlockhash(
// ^? V0TransactionMessage & ITransactionMessageWithFeePayer & TransactionMessageWithBlockhashLifetime
recentBlockhash,
transactionWithFeePayerV0,
transactionMessageWithFeePayer,
);
// ^? V0Transaction & ITransactionWithFeePayer & ITransactionWithBlockhash
```

As you can see, each time a transaction is modified, the type reflects the current state. If you add a fee payer, you’ll get a type representing a transaction with a fee payer, and so on.

Additionally, transaction-modifying methods such as `setTransactionFeePayer(..)` and `setTransactionLifetimeUsingBlockhash(..)` will strip a transaction of its signatures, since those signatures would no longer match the modified transaction message.

```ts
import {
address,
createTransaction,
generateKeyPair,
setTransactionFeePayer,
setTransactionLifetimeUsingBlockhash,
signTransaction,
Blockhash,
} from '@solana/web3.js';

const recentBlockhash = {
blockhash: '4uhcVJyU9pJkvQyS88uRDiswHXSCkY3zQawwpjk2NsNY' as Blockhash,
lastValidBlockHeight: 196055492n,
};
const feePayer = address('AxZfZWeqztBCL37Mkjkd4b8Hf6J13WCcfozrBY6vZzv3');
const signer = await generateKeyPair();
As you can see, each time a transaction message is modified, the type reflects its new shape. If you add a fee payer, you’ll get a type representing a transaction message with a fee payer, and so on.

const transaction = createTransaction({ version: 'legacy' });
const transactionWithFeePayer = setTransactionFeePayer(feePayer, transaction);
const transactionWithFeePayerAndLifetime = setTransactionLifetimeUsingBlockhash(
recentBlockhash,
transactionWithFeePayer,
);
const transactionSignedWithFeePayerAndLifetime = await signTransaction([signer], transactionWithFeePayerAndLifetime);
// ^? LegacyTransaction & ITransactionWithFeePayer & ITransactionWithBlockhash & ITransactionWithSignatures
Transaction message objects are also **frozen by these functions** to prevent them from being mutated in place.

// Setting the lifetime again will remove the signatures from the object
const transactionSignaturesStripped = setTransactionLifetimeUsingBlockhash(
recentBlockhash,
transactionSignedWithFeePayerAndLifetime,
);
// ^? LegacyTransaction & ITransactionWithFeePayer & ITransactionWithBlockhash
```
### Signing Transaction Messages

The `signTransaction(..)` function will raise a type error if your unsigned transaction is not already equipped with a fee payer and a lifetime.
The `signTransaction(..)` function will raise a type error if your transaction message is not already equipped with a fee payer and a lifetime. This helps you catch errors at author-time instead of runtime.

```ts
const feePayer = address('AxZfZWeqztBCL37Mkjkd4b8Hf6J13WCcfozrBY6vZzv3');
const signer = await generateKeyPair();

const transaction = createTransaction({ version: 'legacy' });
const transactionWithFeePayer = setTransactionFeePayer(feePayer, transaction);
const transactionMessage = createTransactionMessage({ version: 'legacy' });
const transactionMessageWithFeePayer = setTransactionMessageFeePayer(feePayer, transactionMessage);

// Attempting to sign the transaction without a lifetime will throw a type error
const transactionSignedWithFeePayer = await signTransaction([signer], transactionWithFeePayer);
// Attempting to sign the transaction message without a lifetime will throw a type error
const signedTransaction = await signTransaction([signer], transactionMessageWithFeePayer);
// => "Property 'lifetimeConstraint' is missing in type"
```

Transaction objects are also **frozen by these functions** to prevent transactions from being mutated in place by functions you pass them to.
### Helpers For Building Transaction Messages

Building transactions in this manner might feel different from what you’re used to. Also, we certainly wouldn’t want you to have to bind transformed transactions to a new variable at each step, so we have released a functional programming library dubbed `@solana/functional` that lets you build transactions in **pipelines**. Here’s how it can be used:
Building transaction messages in this manner might feel different from what you’re used to. Also, we certainly wouldn’t want you to have to bind transformed transaction messages to a new variable at each step, so we have released a functional programming library dubbed `@solana/functional` that lets you build transaction messages in **pipelines**. Here’s how it can be used:

```ts
import { pipe } from '@solana/functional';
import {
address,
createTransactionMessage,
setTransactionMessageFeePayer,
setTransactionMessageLifetimeUsingBlockhash,
Blockhash,
createTransaction,
setTransactionFeePayer,
setTransactionLifetimeUsingBlockhash,
} from '@solana/web3.js';

// Use `pipe(..)` to create a pipeline of transaction transform operations
const transaction = pipe(
createTransaction({ version: 0 }),
tx => setTransactionFeePayer(feePayer, tx),
tx => setTransactionLifetimeUsingBlockhash(recentBlockhash, tx),
// Use `pipe(..)` to create a pipeline of transaction message transformation operations
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
tx => setTransactionMessageFeePayer(feePayer, tx),
tx => setTransactionMessageLifetimeUsingBlockhash(recentBlockhash, tx),
);
```

Note that `pipe(..)` is completely decoupled from transactions, so it can be used to pipeline any compatible transforms.
Note that `pipe(..)` is general-purpose, so it can be used to pipeline any functional transforms.

## Codecs

Expand Down Expand Up @@ -1100,47 +1057,44 @@ const blockWithRewardsAndTransactionsResponse = await rpc

As previously mentioned, the type coverage in web3.js 2.0 allows developers to catch common bugs at compile time, rather than runtime.

In the example below, a transaction is created and then attempted to be compiled 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:
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:

```ts
const encodedTx = pipe(
createTransaction({ version: 0 }),
tx => setTransactionLifetimeUsingBlockhash(recentBlockhash, tx),
tx => getBase64EncodedWireTransaction(tx), // Property 'feePayer' is missing in type
const transactionMessage = pipe(createTransactionMessage({ version: 0 }), tx =>
setTransactionMessageLifetimeUsingBlockhash(recentBlockhash, tx),
);
const signedTransaction = await signTransaction([keyPair], transactionMessage); // ERROR: Property 'feePayer' is missing in type
```

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:

```ts
const unsignedTransaction = pipe(
createTransaction({ version: 0 }),
tx => setTransactionFeePayer(feePayerAddress, tx),
tx => setTransactionLifetimeUsingBlockhash(recentBlockhash, tx),
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
tx => setTransactionMessageFeePayer(feePayerAddress, tx),
tx => setTransactionMessageLifetimeUsingBlockhash(recentBlockhash, tx),
);

const signature = sendAndConfirmTransaction({
confirmRecentTransaction: createDefaultRecentTransactionConfirmer({ rpc, rpcSubscriptions }),
rpc,
transaction: unsignedTransaction, // Transaction has not been signed: Type error
});

const transaction = await signTransaction([], unsignedTransaction);
const signedTransaction = await signTransaction([], transactionMessage);

// Asserts the transaction as a `IFullySignedTransaction`
// Asserts the transaction is a `FullySignedTransaction`
// Throws an error if any signatures are missing!
assertTransactionIsFullySigned(transaction);
assertTransactionIsFullySigned(signedTransaction);

await sendAndConfirmTransaction(signedTransaction);
```

Are you working with a nonce transaction and forgot to make `AdvanceNonce` the first instruction? That’s a type error:
Are you building a nonce transaction and forgot to make `AdvanceNonce` the first instruction? That’s a type error:

```ts
const feePayer = await generateKeyPair();
const feePayerAddress = await getAddressFromPublicKey(feePayer.publicKey);

const notNonceTransaction = pipe(createTransaction({ version: 0 }), tx => setTransactionFeePayer(feePayerAddress, tx));
const notNonceTransactionMessage = pipe(createTransactionMessage({ version: 0 }), tx =>
setTransactionMessageFeePayer(feePayerAddress, tx),
);

notNonceTransaction satisfies IDurableNonceTransaction;
notNonceTransactionMessage satisfies TransactionMessageWithDurableNonceLifetime;
// => Property 'lifetimeConstraint' is missing in type

const nonceConfig = {
Expand All @@ -1149,21 +1103,21 @@ const nonceConfig = {
nonceAuthorityAddress: address('GDhj8paPg8woUzp9n8fj7eAMocN5P7Ej3A7T9F5gotTX'),
};

const stillNotNonceTransaction = {
const stillNotNonceTransactionMessage = {
lifetimeConstraint: nonceConfig,
...notNonceTransaction,
...notNonceTransactionMessage,
};

stillNotNonceTransaction satisfies IDurableNonceTransaction;
stillNotNonceTransactionMessage satisfies TransactionMessageWithDurableNonceLifetime;
// => 'readonly IInstruction<string>[]' is not assignable to type 'readonly [AdvanceNonceAccountInstruction<string, string>, ...IInstruction<string>[]]'

const validNonceTransaction = pipe(
createTransaction({ version: 0 }),
tx => setTransactionFeePayer(feePayerAddress, tx),
tx => setTransactionLifetimeUsingDurableNonce(nonceConfig, tx), // Adds the instruction!
const validNonceTransactionMessage = pipe(
createTransactionMessage({ version: 0 }),
tx => setTransactionMessageFeePayer(feePayerAddress, tx),
tx => setTransactionMessageLifetimeUsingDurableNonce(nonceConfig, tx), // Adds the instruction!
);

validNonceTransaction satisfies IDurableNonceTransaction; // OK
validNonceTransactionMessage satisfies TransactionMessageWithDurableNonceLifetime; // OK
```

The library’s type-checking can even catch you using lamports instead of SOL for a value:
Expand Down Expand Up @@ -1207,15 +1161,8 @@ const cryptoKeyPair: CryptoKeyPair = fromLegacyKeypair(keypair);
Here’s how to convert legacy transaction objects to the new library’s transaction types:

```ts
// For a transaction using a blockhash lifetime
const tx = fromVersionedTransactionWithBlockhash(legacyTransactionV0);
// You can also optionally provide a `lastValidBlockheight` parameter to manage retries
const tx = fromVersionedTransactionWithBlockhash(legacyTransactionV0, lastValidBlockheight);

// For a transaction using a durable nonce lifetime
const tx = fromVersionedTransactionWithDurableNonce(transaction);
// Again you can also optionally provide a `lastValidBlockheight`
const tx = fromVersionedTransactionWithDurableNonce(transaction, lastValidBlockheight);
// Note that you can only convert `VersionedTransaction` objects
const modernTransaction = fromVersionedTransaction(classicTransaction);
```

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).
Expand All @@ -1226,10 +1173,10 @@ Writing JavaScript clients for on-chain programs has been done manually up until

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.

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 composed of instructions from three different core programs.
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.

```ts
import { createTransaction, pipe } from '@solana/web3.js';
import { appendTransactionMessageInstructions, createTransactionMessage, pipe } from '@solana/web3.js';
import { getAddMemoInstruction } from '@solana-program/memo';
import { getSetComputeUnitLimitInstruction } from '@solana-program/compute-budget';
import { getTransferSolInstruction } from '@solana-program/system';
Expand All @@ -1240,8 +1187,10 @@ const instructions = [
getAddMemoInstruction({ memo: "I'm transferring some SOL!" }),
];

// Creates a V0 transaction with 3 instructions inside.
const transaction = pipe(createTransaction({ version: 0 }), tx => appendTransactionInstructions(instructions, tx));
// Creates a V0 transaction message with 3 instructions inside.
const transactionMessage = pipe(createTransactionMessage({ version: 0 }), tx =>
appendTransactionMessageInstructions(instructions, tx),
);
```

As you can see, each program now generates its own library allowing you to cherry-pick your dependencies.
Expand Down
2 changes: 1 addition & 1 deletion packages/transaction-messages/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ const nonce =
// value, and furthermore asked for it in `base58` encoding.
nonceAccount!.data[0] as unknown as Nonce;

const durableNonceTransaction = setTransactionLifetimeUsingDurableNonce(
const durableNonceTransactionMessage = setTransactionMessageLifetimeUsingDurableNonce(
{ nonce, nonceAccountAddress, nonceAuthorityAddress },
tx,
);
Expand Down

0 comments on commit 20e490a

Please sign in to comment.