Skip to content

Make FullySignedTransaction composable #462

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/sour-beans-hear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@solana/transactions': major
'@solana/signers': major
'@solana/kit': major
---

BREAKING CHANGE: The `FullySignedTransaction` no longer extends the `Transaction` type so it can be composed with other flags that also narrow transaction types. This means, whenever `FullySignedTransaction` is used on its own, it will need to be replaced with `FullySignedTransaction & Transaction`.
14 changes: 8 additions & 6 deletions docs/content/docs/getting-started/send-transaction.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ The most common way to send transactions is via the `sendTransaction` RPC method
One way to tackle this would be to encode the transaction ourselves using `getBase64EncodedWireTransaction` and pass it to the `sendTransaction` RPC method like so.

```ts twoslash
import { FullySignedTransaction, TransactionWithBlockhashLifetime, Rpc, SolanaRpcApi } from "@solana/kit";
const signedTransaction = null as unknown as FullySignedTransaction & TransactionWithBlockhashLifetime;
import { Transaction, FullySignedTransaction, TransactionWithBlockhashLifetime, Rpc, SolanaRpcApi } from "@solana/kit";
const signedTransaction = null as unknown as Transaction & FullySignedTransaction & TransactionWithBlockhashLifetime;
const rpc = null as unknown as Rpc<SolanaRpcApi>;
// ---cut-before---
import { getBase64EncodedWireTransaction } from "@solana/kit";
Expand All @@ -25,8 +25,8 @@ await rpc.sendTransaction(encodedTransaction, { preflightCommitment: "confirmed"
However, Kit offers a helper function that does that for us whilst providing some sensible default values. This function is called `sendTransactionWithoutConfirmingFactory` and, given an RPC object, it returns a function that sends transactions without waiting for confirmation.

```ts twoslash
import { FullySignedTransaction, TransactionWithBlockhashLifetime, Rpc, SolanaRpcApi } from "@solana/kit";
const signedTransaction = null as unknown as FullySignedTransaction & TransactionWithBlockhashLifetime;
import { Transaction, FullySignedTransaction, TransactionWithBlockhashLifetime, Rpc, SolanaRpcApi } from "@solana/kit";
const signedTransaction = null as unknown as Transaction & FullySignedTransaction & TransactionWithBlockhashLifetime;
const rpc = null as unknown as Rpc<SolanaRpcApi>;
// ---cut-before---
import { sendTransactionWithoutConfirmingFactory } from "@solana/kit";
Expand All @@ -50,14 +50,15 @@ Both accept RPC and RPC Subscriptions objects and return a function that sends a

```ts twoslash
import {
Transaction,
FullySignedTransaction,
TransactionWithBlockhashLifetime,
Rpc,
SolanaRpcApi,
RpcSubscriptions,
SolanaRpcSubscriptionsApi,
} from "@solana/kit";
const signedTransaction = null as unknown as FullySignedTransaction & TransactionWithBlockhashLifetime;
const signedTransaction = null as unknown as Transaction & FullySignedTransaction & TransactionWithBlockhashLifetime;
const rpc = null as unknown as Rpc<SolanaRpcApi>;
const rpcSubscriptions = null as unknown as RpcSubscriptions<SolanaRpcSubscriptionsApi>;
// ---cut-before---
Expand All @@ -77,14 +78,15 @@ As such, Kit decouples these two distinct concepts by offering a `getSignatureFr

```ts twoslash
import {
Transaction,
FullySignedTransaction,
TransactionWithBlockhashLifetime,
Rpc,
SolanaRpcApi,
RpcSubscriptions,
SolanaRpcSubscriptionsApi,
} from "@solana/kit";
const signedTransaction = null as unknown as FullySignedTransaction & TransactionWithBlockhashLifetime;
const signedTransaction = null as unknown as Transaction & FullySignedTransaction & TransactionWithBlockhashLifetime;
const rpc = null as unknown as Rpc<SolanaRpcApi>;
const rpcSubscriptions = null as unknown as RpcSubscriptions<SolanaRpcSubscriptionsApi>;
// ---cut-before---
Expand Down
3 changes: 2 additions & 1 deletion docs/content/docs/upgrade-guide.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -473,10 +473,11 @@ import {
createSolanaRpcSubscriptions,
FullySignedTransaction,
TransactionWithBlockhashLifetime,
Transaction,
} from "@solana/kit";
const rpc = createSolanaRpc("https://api.devnet.solana.com");
const rpcSubscriptions = createSolanaRpcSubscriptions("wss://api.devnet.solana.com");
const signedTransaction = null as unknown as FullySignedTransaction & TransactionWithBlockhashLifetime;
const signedTransaction = null as unknown as Transaction & FullySignedTransaction & TransactionWithBlockhashLifetime;
// ---cut-end---

// Create a send and confirm function from your RPC and RPC Subscriptions objects.
Expand Down
4 changes: 3 additions & 1 deletion packages/kit/src/__tests__/send-transaction-internal-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
Base64EncodedWireTransaction,
FullySignedTransaction,
getBase64EncodedWireTransaction,
Transaction,
TransactionWithBlockhashLifetime,
TransactionWithDurableNonceLifetime,
} from '@solana/transactions';
Expand All @@ -21,7 +22,7 @@ const FOREVER_PROMISE = new Promise(() => {
});

describe('sendAndConfirmTransaction', () => {
const MOCK_TRANSACTION = {} as FullySignedTransaction & TransactionWithBlockhashLifetime;
const MOCK_TRANSACTION = {} as FullySignedTransaction & Transaction & TransactionWithBlockhashLifetime;
let confirmRecentTransaction: jest.Mock;
let createPendingRequest: jest.Mock;
let rpc: Rpc<SendTransactionApi>;
Expand Down Expand Up @@ -176,6 +177,7 @@ describe('sendAndConfirmTransaction', () => {

describe('sendAndConfirmDurableNonceTransaction', () => {
const MOCK_DURABLE_NONCE_TRANSACTION = {} as unknown as FullySignedTransaction &
Transaction &
TransactionWithDurableNonceLifetime;
let confirmDurableNonceTransaction: jest.Mock;
let createPendingRequest: jest.Mock;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import {
createRecentSignatureConfirmationPromiseFactory,
waitForDurableNonceTransactionConfirmation,
} from '@solana/transaction-confirmation';
import { FullySignedTransaction, TransactionWithDurableNonceLifetime } from '@solana/transactions';
import { FullySignedTransaction, Transaction, TransactionWithDurableNonceLifetime } from '@solana/transactions';

import { sendAndConfirmDurableNonceTransaction_INTERNAL_ONLY_DO_NOT_EXPORT } from './send-transaction-internal';

type SendAndConfirmDurableNonceTransactionFunction = (
transaction: FullySignedTransaction & TransactionWithDurableNonceLifetime,
transaction: FullySignedTransaction & Transaction & TransactionWithDurableNonceLifetime,
config: Omit<
Parameters<typeof sendAndConfirmDurableNonceTransaction_INTERNAL_ONLY_DO_NOT_EXPORT>[0],
'confirmDurableNonceTransaction' | 'rpc' | 'transaction'
Expand Down
4 changes: 2 additions & 2 deletions packages/kit/src/send-and-confirm-transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import {
TransactionWithLastValidBlockHeight,
waitForRecentTransactionConfirmation,
} from '@solana/transaction-confirmation';
import { FullySignedTransaction } from '@solana/transactions';
import { FullySignedTransaction, Transaction } from '@solana/transactions';

import { sendAndConfirmTransactionWithBlockhashLifetime_INTERNAL_ONLY_DO_NOT_EXPORT } from './send-transaction-internal';

type SendAndConfirmTransactionWithBlockhashLifetimeFunction = (
transaction: FullySignedTransaction & TransactionWithLastValidBlockHeight,
transaction: FullySignedTransaction & Transaction & TransactionWithLastValidBlockHeight,
config: Omit<
Parameters<typeof sendAndConfirmTransactionWithBlockhashLifetime_INTERNAL_ONLY_DO_NOT_EXPORT>[0],
'confirmRecentTransaction' | 'rpc' | 'transaction'
Expand Down
7 changes: 4 additions & 3 deletions packages/kit/src/send-transaction-internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import {
FullySignedTransaction,
getBase64EncodedWireTransaction,
Transaction,
TransactionWithDurableNonceLifetime,
} from '@solana/transactions';

Expand All @@ -21,7 +22,7 @@ interface SendAndConfirmDurableNonceTransactionConfig
'getNonceInvalidationPromise' | 'getRecentSignatureConfirmationPromise'
>,
) => Promise<void>;
transaction: FullySignedTransaction & TransactionWithDurableNonceLifetime;
transaction: FullySignedTransaction & Transaction & TransactionWithDurableNonceLifetime;
}

interface SendAndConfirmTransactionWithBlockhashLifetimeConfig
Expand All @@ -33,14 +34,14 @@ interface SendAndConfirmTransactionWithBlockhashLifetimeConfig
'getBlockHeightExceedencePromise' | 'getRecentSignatureConfirmationPromise'
>,
) => Promise<void>;
transaction: FullySignedTransaction & TransactionWithLastValidBlockHeight;
transaction: FullySignedTransaction & Transaction & TransactionWithLastValidBlockHeight;
}

interface SendTransactionBaseConfig extends SendTransactionConfigWithoutEncoding {
abortSignal?: AbortSignal;
commitment: Commitment;
rpc: Rpc<SendTransactionApi>;
transaction: FullySignedTransaction;
transaction: FullySignedTransaction & Transaction;
}

type SendTransactionConfigWithoutEncoding = Omit<
Expand Down
4 changes: 2 additions & 2 deletions packages/kit/src/send-transaction-without-confirming.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { Rpc, SendTransactionApi } from '@solana/rpc';
import { FullySignedTransaction } from '@solana/transactions';
import { FullySignedTransaction, Transaction } from '@solana/transactions';

import { sendTransaction_INTERNAL_ONLY_DO_NOT_EXPORT } from './send-transaction-internal';

type SendTransactionWithoutConfirmingFunction = (
transaction: FullySignedTransaction,
transaction: FullySignedTransaction & Transaction,
config: Omit<Parameters<typeof sendTransaction_INTERNAL_ONLY_DO_NOT_EXPORT>[0], 'rpc' | 'transaction'>,
) => Promise<void>;

Expand Down
2 changes: 1 addition & 1 deletion packages/signers/src/__tests__/sign-transaction-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,7 @@ describe('signTransactionMessageWithSigners', () => {
});

// And the transaction is fully signed.
signedTransaction satisfies FullySignedTransaction;
signedTransaction satisfies FullySignedTransaction & Transaction;

// And the signers were called with the expected parameters.
expect(signerA.modifyAndSignTransactions).toHaveBeenCalledWith([unsignedTransaction], mockOptions);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ type CompilableTransactionMessageWithSigners = CompilableTransactionMessage & Tr
const transactionMessage = null as unknown as CompilableTransactionMessageWithSigners &
TransactionMessageWithBlockhashLifetime;
signTransactionMessageWithSigners(transactionMessage) satisfies Promise<
Readonly<FullySignedTransaction & TransactionWithBlockhashLifetime>
Readonly<FullySignedTransaction & Transaction & TransactionWithBlockhashLifetime>
>;
}

Expand All @@ -74,15 +74,15 @@ type CompilableTransactionMessageWithSigners = CompilableTransactionMessage & Tr
const transactionMessage = null as unknown as CompilableTransactionMessageWithSigners &
TransactionMessageWithDurableNonceLifetime;
signTransactionMessageWithSigners(transactionMessage) satisfies Promise<
Readonly<FullySignedTransaction & TransactionWithDurableNonceLifetime>
Readonly<FullySignedTransaction & Transaction & TransactionWithDurableNonceLifetime>
>;
}

{
// [signTransactionMessageWithSigners]: returns a fully signed transaction with an unknown lifetime
const transactionMessage = null as unknown as CompilableTransactionMessageWithSigners;
signTransactionMessageWithSigners(transactionMessage) satisfies Promise<
Readonly<FullySignedTransaction & TransactionWithLifetime>
Readonly<FullySignedTransaction & Transaction & TransactionWithLifetime>
>;
}

Expand Down
2 changes: 1 addition & 1 deletion packages/signers/src/sign-transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ export async function signTransactionMessageWithSigners<
*
*/
export async function signAndSendTransactionMessageWithSigners<
TTransactionMessage extends CompilableTransactionMessageWithSigners = CompilableTransactionMessageWithSigners,
TTransactionMessage extends CompilableTransactionMessageWithSigners,
>(transaction: TTransactionMessage, config?: TransactionSendingSignerConfig): Promise<SignatureBytes> {
assertIsTransactionMessageWithSingleSendingSigner(transaction);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { Transaction } from '../transaction';
// signTransaction
{
const transaction = null as unknown as Transaction & { some: 1 };
signTransaction([], transaction) satisfies Promise<FullySignedTransaction & { some: 1 }>;
signTransaction([], transaction) satisfies Promise<FullySignedTransaction & Transaction & { some: 1 }>;
}

// isFullySignedTransaction
Expand Down
14 changes: 7 additions & 7 deletions packages/transactions/src/signatures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { Transaction } from './transaction';
* Represents a transaction that is signed by all of its required signers. Being fully signed is a
* prerequisite of functions designed to land transactions on the network.
*/
export type FullySignedTransaction = NominalType<'transactionSignedness', 'fullySigned'> & Transaction;
export type FullySignedTransaction = NominalType<'transactionSignedness', 'fullySigned'>;

let base58Decoder: Decoder<string> | undefined;

Expand Down Expand Up @@ -70,10 +70,10 @@ function uint8ArraysEqual(arr1: Uint8Array, arr2: Uint8Array) {
* @see {@link signTransaction} if you want to assert that the transaction has all of its required
* signatures after signing.
*/
export async function partiallySignTransaction<T extends Transaction>(
export async function partiallySignTransaction<TTransaction extends Transaction>(
keyPairs: CryptoKeyPair[],
transaction: T,
): Promise<T> {
transaction: TTransaction,
): Promise<TTransaction> {
let newSignatures: Record<Address, SignatureBytes> | undefined;
let unexpectedSigners: Set<Address> | undefined;

Expand Down Expand Up @@ -146,10 +146,10 @@ export async function partiallySignTransaction<T extends Transaction>(
* @see {@link partiallySignTransaction} if you want to sign the transaction without asserting that
* the resulting transaction is fully signed.
*/
export async function signTransaction<T extends Transaction>(
export async function signTransaction<TTransaction extends Transaction>(
keyPairs: CryptoKeyPair[],
transaction: T,
): Promise<FullySignedTransaction & T> {
transaction: TTransaction,
): Promise<FullySignedTransaction & TTransaction> {
const out = await partiallySignTransaction(keyPairs, transaction);
assertIsFullySignedTransaction(out);
Object.freeze(out);
Expand Down