Skip to content
This repository has been archived by the owner on Jan 22, 2025. It is now read-only.

Commit

Permalink
refactor(experimental): add function to check if a transaction is ful…
Browse files Browse the repository at this point in the history
…ly signed

- asserts that there is a signature for each expected signer
- only verifies the presence of signatures, does not verify the signatures are valid
  • Loading branch information
mcintyre94 committed Oct 25, 2023
1 parent d63fb95 commit 193b243
Show file tree
Hide file tree
Showing 2 changed files with 260 additions and 1 deletion.
244 changes: 243 additions & 1 deletion packages/transactions/src/__tests__/signatures-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,18 @@ import {
getAddressEncoder,
getAddressFromPublicKey,
} from '@solana/addresses';
import { AccountRole } from '@solana/instructions';
import { Ed25519Signature, signBytes } from '@solana/keys';

import { Blockhash } from '../blockhash';
import { CompiledMessage, compileMessage } from '../message';
import { assertIsTransactionSignature, getSignatureFromTransaction, signTransaction } from '../signatures';
import {
assertIsTransactionSignature,
assertTransactionIsFullySigned,
getSignatureFromTransaction,
ITransactionWithSignatures,
signTransaction,
} from '../signatures';

jest.mock('@solana/addresses');
jest.mock('@solana/keys');
Expand Down Expand Up @@ -275,3 +282,238 @@ describe('signTransaction', () => {
await expect(signTransaction([mockKeyPairA], MOCK_TRANSACTION)).resolves.toBeFrozenObject();
});
});

describe('assertTransactionIsFullySigned', () => {
type SignedTransaction = Parameters<typeof compileMessage>[0] & ITransactionWithSignatures;

const mockProgramAddress = 'program' as Base58EncodedAddress;
const mockPublicKeyAddressA = 'A' as Base58EncodedAddress;
const mockSignatureA = new Uint8Array(0) as Ed25519Signature;
const mockPublicKeyAddressB = 'B' as Base58EncodedAddress;
const mockSignatureB = new Uint8Array(1) as Ed25519Signature;
const mockPublicKeyAddressC = 'C' as Base58EncodedAddress;
const mockSignatureC = new Uint8Array(2) as Ed25519Signature;

const mockBlockhashConstraint = {
blockhash: 'a' as Blockhash,
lastValidBlockHeight: 100n,
};

it('throws if the transaction has no signature for the fee payer', () => {
const transaction: SignedTransaction = {
feePayer: mockPublicKeyAddressA,
instructions: [],
lifetimeConstraint: mockBlockhashConstraint,
signatures: {},
version: 0,
};

expect(() => assertTransactionIsFullySigned(transaction)).toThrow(
'Transaction is missing signature for address A'
);
});

it('throws if the transaction has no signature for an instruction readonly signer', () => {
const transaction: SignedTransaction = {
feePayer: mockPublicKeyAddressA,
instructions: [
{
accounts: [
{
address: mockPublicKeyAddressB,
role: AccountRole.READONLY_SIGNER,
},
],
programAddress: mockProgramAddress,
},
],
lifetimeConstraint: mockBlockhashConstraint,
signatures: {
[mockPublicKeyAddressA]: mockSignatureA,
},
version: 0,
};

expect(() => assertTransactionIsFullySigned(transaction)).toThrow(
'Transaction is missing signature for address B'
);
});

it('throws if the transaction has no signature for an instruction writable signer', () => {
const transaction: SignedTransaction = {
feePayer: mockPublicKeyAddressA,
instructions: [
{
accounts: [
{
address: mockPublicKeyAddressB,
role: AccountRole.WRITABLE_SIGNER,
},
],
programAddress: mockProgramAddress,
},
],
lifetimeConstraint: mockBlockhashConstraint,
signatures: {
[mockPublicKeyAddressA]: mockSignatureA,
},
version: 0,
};

expect(() => assertTransactionIsFullySigned(transaction)).toThrow(
'Transaction is missing signature for address B'
);
});

it('throws if the transaction has multiple instructions and one is missing a signer', () => {
const transaction: SignedTransaction = {
feePayer: mockPublicKeyAddressA,
instructions: [
{
accounts: [
{
address: mockPublicKeyAddressB,
role: AccountRole.WRITABLE_SIGNER,
},
],
programAddress: mockProgramAddress,
},
{
accounts: [
{
address: mockPublicKeyAddressC,
role: AccountRole.WRITABLE_SIGNER,
},
],
programAddress: mockProgramAddress,
},
],
lifetimeConstraint: mockBlockhashConstraint,
signatures: {
[mockPublicKeyAddressA]: mockSignatureA,
[mockPublicKeyAddressB]: mockSignatureB,
},
version: 0,
};

expect(() => assertTransactionIsFullySigned(transaction)).toThrow(
'Transaction is missing signature for address C'
);
});

it('does not throw if the transaction has no instructions and is signed by the fee payer', () => {
const transaction: SignedTransaction = {
feePayer: mockPublicKeyAddressA,
instructions: [],
lifetimeConstraint: mockBlockhashConstraint,
signatures: {
[mockPublicKeyAddressA]: mockSignatureA,
},
version: 0,
};

expect(() => assertTransactionIsFullySigned(transaction)).not.toThrow();
});

it('does not throw if the transaction has an instruction and is signed by the fee payer and instruction signer', () => {
const transaction: SignedTransaction = {
feePayer: mockPublicKeyAddressA,
instructions: [
{
accounts: [
{
address: mockPublicKeyAddressB,
role: AccountRole.WRITABLE_SIGNER,
},
],
programAddress: mockProgramAddress,
},
],
lifetimeConstraint: mockBlockhashConstraint,
signatures: {
[mockPublicKeyAddressA]: mockSignatureA,
[mockPublicKeyAddressB]: mockSignatureB,
},
version: 0,
};

expect(() => assertTransactionIsFullySigned(transaction)).not.toThrow();
});

it('does not throw if the transaction has multiple instructions and is signed by all signers', () => {
const transaction: SignedTransaction = {
feePayer: mockPublicKeyAddressA,
instructions: [
{
accounts: [
{
address: mockPublicKeyAddressB,
role: AccountRole.WRITABLE_SIGNER,
},
],
programAddress: mockProgramAddress,
},
{
accounts: [
{
address: mockPublicKeyAddressC,
role: AccountRole.WRITABLE_SIGNER,
},
],
programAddress: mockProgramAddress,
},
],
lifetimeConstraint: mockBlockhashConstraint,
signatures: {
[mockPublicKeyAddressA]: mockSignatureA,
[mockPublicKeyAddressB]: mockSignatureB,
[mockPublicKeyAddressC]: mockSignatureC,
},
version: 0,
};

expect(() => assertTransactionIsFullySigned(transaction)).not.toThrow();
});

it('does not throw if the transaction has an instruction with a non-signer account', () => {
const transaction: SignedTransaction = {
feePayer: mockPublicKeyAddressA,
instructions: [
{
accounts: [
{
address: mockPublicKeyAddressB,
role: AccountRole.WRITABLE,
},
],
programAddress: mockProgramAddress,
},
],
lifetimeConstraint: mockBlockhashConstraint,
signatures: {
[mockPublicKeyAddressA]: mockSignatureA,
},
version: 0,
};

expect(() => assertTransactionIsFullySigned(transaction)).not.toThrow();
});

it('does not throw if the transaction has an instruction with no accounts', () => {
const transaction: SignedTransaction = {
feePayer: mockPublicKeyAddressA,
instructions: [
{
programAddress: mockProgramAddress,
},
],
lifetimeConstraint: mockBlockhashConstraint,
signatures: {
[mockPublicKeyAddressA]: mockSignatureA,
},
version: 0,
};

expect(() => assertTransactionIsFullySigned(transaction)).not.toThrow();
});
});
17 changes: 17 additions & 0 deletions packages/transactions/src/signatures.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { base58 } from '@metaplex-foundation/umi-serializers';
import { Base58EncodedAddress, getAddressFromPublicKey } from '@solana/addresses';
import { isSignerRole } from '@solana/instructions';
import { Ed25519Signature, signBytes } from '@solana/keys';

import { ITransactionWithFeePayer } from './fee-payer';
Expand Down Expand Up @@ -113,3 +114,19 @@ export function transactionSignature(putativeTransactionSignature: string): Tran
assertIsTransactionSignature(putativeTransactionSignature);
return putativeTransactionSignature;
}

export function assertTransactionIsFullySigned<TTransaction extends Parameters<typeof compileMessage>[0]>(
transaction: TTransaction & ITransactionWithSignatures
): asserts transaction is TTransaction & IFullySignedTransaction {
const signerAddressesFromInstructions = transaction.instructions
.flatMap(i => i.accounts?.filter(a => isSignerRole(a.role)) ?? [])
.map(a => a.address);
const requiredSigners = [transaction.feePayer, ...signerAddressesFromInstructions];

requiredSigners.forEach(address => {
if (!transaction.signatures[address]) {
// TODO coded error
throw new Error(`Transaction is missing signature for address ${address}`);
}
});
}

0 comments on commit 193b243

Please sign in to comment.