From 205c09268ce4c6989d9fb838c4cf7d0000d9ef32 Mon Sep 17 00:00:00 2001 From: Steven Luscher Date: Tue, 7 Nov 2023 16:53:59 -0800 Subject: [PATCH] refactor(experimental): rename `Ed25519Signature` and `TransactionSignature` to `SignatureBytes` and `Signature` (#1815) --- .../compat/src/__tests__/transaction-test.ts | 26 ++-- packages/compat/src/transaction.ts | 4 +- packages/keys/README.md | 65 ++++++++- packages/keys/package.json | 4 +- packages/keys/src/__tests__/coercions-test.ts | 17 +++ .../keys/src/__tests__/signatures-test.ts | 132 +++++++++++++++++- .../src/__typetests__/coercions-typetests.ts | 3 + packages/keys/src/signatures.ts | 65 ++++++++- packages/keys/tsconfig.json | 2 +- packages/library/README.md | 2 +- .../src/__tests__/airdrop-confirmer-test.ts | 4 +- .../library/src/__tests__/airdrop-test.ts | 12 +- .../src/__tests__/send-transaction-test.ts | 6 +- ...action-confirmation-strategy-racer-test.ts | 10 +- ...on-confirmation-strategy-signature-test.ts | 20 +-- .../transaction-confirmation-test.ts | 16 +-- packages/library/src/airdrop-confirmer.ts | 4 +- packages/library/src/airdrop.ts | 8 +- packages/library/src/send-transaction.ts | 12 +- ...transaction-confirmation-strategy-racer.ts | 4 +- ...-confirmation-strategy-recent-signature.ts | 4 +- packages/rpc-core/package.json | 1 + .../__tests__/get-signature-statuses-test.ts | 8 +- packages/rpc-core/src/rpc-methods/common.ts | 2 - .../src/rpc-methods/getSignatureStatuses.ts | 4 +- .../rpc-methods/getSignaturesForAddress.ts | 8 +- .../src/rpc-methods/getTransaction.ts | 10 +- .../src/rpc-methods/requestAirdrop.ts | 5 +- .../src/rpc-methods/sendTransaction.ts | 5 +- .../logs-notifications-type-test.ts | 4 +- .../signature-notifications-type-test.ts | 22 +-- .../vote-notifications-type-test.ts | 5 +- .../rpc-subscriptions/logs-notifications.ts | 4 +- .../signature-notifications.ts | 6 +- .../rpc-subscriptions/vote-notifications.ts | 4 +- packages/rpc-graphql/package.json | 1 + .../src/schema/transaction/query.ts | 5 +- .../rpc-types/src/__tests__/coercions-test.ts | 17 --- .../src/__typetests__/coercions-typetests.ts | 3 - packages/transactions/README.md | 44 +----- .../__tests__/decompile-transaction-test.ts | 32 ++--- .../src/__tests__/signatures-test.ts | 132 +----------------- .../src/__tests__/wire-transaction-test.ts | 4 +- .../transactions/src/compile-transaction.ts | 4 +- .../transactions/src/decompile-transaction.ts | 4 +- .../src/serializers/transaction.ts | 6 +- packages/transactions/src/signatures.ts | 73 +--------- pnpm-lock.yaml | 12 ++ 48 files changed, 439 insertions(+), 406 deletions(-) create mode 100644 packages/keys/src/__tests__/coercions-test.ts create mode 100644 packages/keys/src/__typetests__/coercions-typetests.ts diff --git a/packages/compat/src/__tests__/transaction-test.ts b/packages/compat/src/__tests__/transaction-test.ts index 7dbd0517055a..9725be5d8901 100644 --- a/packages/compat/src/__tests__/transaction-test.ts +++ b/packages/compat/src/__tests__/transaction-test.ts @@ -1,6 +1,6 @@ import { Address } from '@solana/addresses'; import { AccountRole, IInstruction } from '@solana/instructions'; -import { Ed25519Signature } from '@solana/keys'; +import { SignatureBytes } from '@solana/keys'; import { ITransactionWithSignatures, Nonce } from '@solana/transactions'; import { PublicKey, TransactionInstruction, TransactionMessage, VersionedTransaction } from '@solana/web3.js'; @@ -186,7 +186,7 @@ describe('fromVersionedTransactionWithBlockhash', () => { ) as unknown as ITransactionWithSignatures; expect(transaction.signatures).toStrictEqual({ - '7EqQdEULxWcraVx3mXKFjc84LhCkMGZCkRuDpvcMwJeK': feePayerSignature as Ed25519Signature, + '7EqQdEULxWcraVx3mXKFjc84LhCkMGZCkRuDpvcMwJeK': feePayerSignature as SignatureBytes, }); }); @@ -234,8 +234,8 @@ describe('fromVersionedTransactionWithBlockhash', () => { expect(transaction.signatures).toStrictEqual({ '3LeBzRE9Yna5zi9R8vdT3MiNQYuEp4gJgVyhhwmqfCtd': new Uint8Array(Array(64).fill(3)), - '7EqQdEULxWcraVx3mXKFjc84LhCkMGZCkRuDpvcMwJeK': new Uint8Array(Array(64).fill(1)) as Ed25519Signature, - '8kud9bpNvfemXYdTFjs5cZ8fZinBkx8JAnhVmRwJZk5e': new Uint8Array(Array(64).fill(2)) as Ed25519Signature, + '7EqQdEULxWcraVx3mXKFjc84LhCkMGZCkRuDpvcMwJeK': new Uint8Array(Array(64).fill(1)) as SignatureBytes, + '8kud9bpNvfemXYdTFjs5cZ8fZinBkx8JAnhVmRwJZk5e': new Uint8Array(Array(64).fill(2)) as SignatureBytes, }); }); @@ -476,7 +476,7 @@ describe('fromVersionedTransactionWithBlockhash', () => { ) as unknown as ITransactionWithSignatures; expect(transaction.signatures).toStrictEqual({ - '7EqQdEULxWcraVx3mXKFjc84LhCkMGZCkRuDpvcMwJeK': feePayerSignature as Ed25519Signature, + '7EqQdEULxWcraVx3mXKFjc84LhCkMGZCkRuDpvcMwJeK': feePayerSignature as SignatureBytes, }); }); @@ -524,8 +524,8 @@ describe('fromVersionedTransactionWithBlockhash', () => { expect(transaction.signatures).toStrictEqual({ '3LeBzRE9Yna5zi9R8vdT3MiNQYuEp4gJgVyhhwmqfCtd': new Uint8Array(Array(64).fill(3)), - '7EqQdEULxWcraVx3mXKFjc84LhCkMGZCkRuDpvcMwJeK': new Uint8Array(Array(64).fill(1)) as Ed25519Signature, - '8kud9bpNvfemXYdTFjs5cZ8fZinBkx8JAnhVmRwJZk5e': new Uint8Array(Array(64).fill(2)) as Ed25519Signature, + '7EqQdEULxWcraVx3mXKFjc84LhCkMGZCkRuDpvcMwJeK': new Uint8Array(Array(64).fill(1)) as SignatureBytes, + '8kud9bpNvfemXYdTFjs5cZ8fZinBkx8JAnhVmRwJZk5e': new Uint8Array(Array(64).fill(2)) as SignatureBytes, }); }); @@ -781,7 +781,7 @@ describe('fromVersionedTransactionWithDurableNonce', () => { ) as unknown as ITransactionWithSignatures; expect(transaction.signatures).toStrictEqual({ - '2KntmCrnaf63tpNb8UMFFjFGGnYYAKQdmW9SbuCiRvhM': signature as Ed25519Signature, + '2KntmCrnaf63tpNb8UMFFjFGGnYYAKQdmW9SbuCiRvhM': signature as SignatureBytes, }); }); @@ -807,8 +807,8 @@ describe('fromVersionedTransactionWithDurableNonce', () => { ) as unknown as ITransactionWithSignatures; expect(transaction.signatures).toStrictEqual({ - '2KntmCrnaf63tpNb8UMFFjFGGnYYAKQdmW9SbuCiRvhM': nonceAuthoritySignature as Ed25519Signature, - '7EqQdEULxWcraVx3mXKFjc84LhCkMGZCkRuDpvcMwJeK': feePayerSignature as Ed25519Signature, + '2KntmCrnaf63tpNb8UMFFjFGGnYYAKQdmW9SbuCiRvhM': nonceAuthoritySignature as SignatureBytes, + '7EqQdEULxWcraVx3mXKFjc84LhCkMGZCkRuDpvcMwJeK': feePayerSignature as SignatureBytes, }); }); @@ -1013,7 +1013,7 @@ describe('fromVersionedTransactionWithDurableNonce', () => { ) as unknown as ITransactionWithSignatures; expect(transaction.signatures).toStrictEqual({ - '2KntmCrnaf63tpNb8UMFFjFGGnYYAKQdmW9SbuCiRvhM': signature as Ed25519Signature, + '2KntmCrnaf63tpNb8UMFFjFGGnYYAKQdmW9SbuCiRvhM': signature as SignatureBytes, }); }); @@ -1039,8 +1039,8 @@ describe('fromVersionedTransactionWithDurableNonce', () => { ) as unknown as ITransactionWithSignatures; expect(transaction.signatures).toStrictEqual({ - '2KntmCrnaf63tpNb8UMFFjFGGnYYAKQdmW9SbuCiRvhM': nonceAuthoritySignature as Ed25519Signature, - '7EqQdEULxWcraVx3mXKFjc84LhCkMGZCkRuDpvcMwJeK': feePayerSignature as Ed25519Signature, + '2KntmCrnaf63tpNb8UMFFjFGGnYYAKQdmW9SbuCiRvhM': nonceAuthoritySignature as SignatureBytes, + '7EqQdEULxWcraVx3mXKFjc84LhCkMGZCkRuDpvcMwJeK': feePayerSignature as SignatureBytes, }); }); diff --git a/packages/compat/src/transaction.ts b/packages/compat/src/transaction.ts index 419d9f5a61bb..ab143b844ae1 100644 --- a/packages/compat/src/transaction.ts +++ b/packages/compat/src/transaction.ts @@ -2,7 +2,7 @@ import { type Address, assertIsAddress } from '@solana/addresses'; import { pipe } from '@solana/functional'; import type { IAccountMeta, IInstruction } from '@solana/instructions'; import { AccountRole } from '@solana/instructions'; -import type { Ed25519Signature } from '@solana/keys'; +import type { SignatureBytes } from '@solana/keys'; import { appendTransactionInstruction, type Blockhash, @@ -86,7 +86,7 @@ function convertSignatures( if (allZeros) return acc; const address = staticAccountKeys[index].toBase58() as Address; - return { ...acc, [address]: sig as Ed25519Signature }; + return { ...acc, [address]: sig as SignatureBytes }; }, {}); } diff --git a/packages/keys/README.md b/packages/keys/README.md index b6e014c59de6..e2a953dbcc9f 100644 --- a/packages/keys/README.md +++ b/packages/keys/README.md @@ -18,7 +18,11 @@ This package contains utilities for validating, generating, and manipulating add ## Types -### `Ed25519Signature` +### `Signature` + +This type represents a 64-byte Ed25519 signature of some data with a private key, as a base58-encoded string. + +### `SignatureBytes` This type represents a 64-byte Ed25519 signature of some data with a private key. @@ -26,6 +30,31 @@ Whenever you need to verify that a particular signature is, in fact, the one tha ## Functions +### `assertIsSignature()` + +From time to time you might acquire a string that you expect to be a base58-encoded signature (eg. of a transaction) from an untrusted network API or user input. To assert that such an arbitrary string is in fact an Ed25519 signature, use the `assertIsSignature` function. + +```ts +import { assertIsSignature } from '@solana/keys'; + +// Imagine a function that asserts whether a user-supplied signature is valid or not. +function handleSubmit() { + // We know only that what the user typed conforms to the `string` type. + const signature: string = signatureInput.value; + try { + // If this type assertion function doesn't throw, then + // Typescript will upcast `signature` to `Signature`. + assertIsSignature(signature); + // At this point, `signature` is a `Signature` that can be used with the RPC. + const { + value: [status], + } = await rpc.getSignatureStatuses([signature]).send(); + } catch (e) { + // `signature` turned out not to be a base58-encoded signature + } +} +``` + ### `generateKeyPair()` Generates an Ed25519 public/private key pair for use with other methods in this package that accept `CryptoKey` objects. @@ -36,6 +65,25 @@ import { generateKeyPair } from '@solana/keys'; const { privateKey, publicKey } = await generateKeyPair(); ``` +### `isSignature()` + +This is a type guard that accepts a string as input. It will both return `true` if the string conforms to the `Signature` type and will refine the type for use in your program. + +```ts +import { isSignature } from '@solana/keys'; + +if (isSignature(signature)) { + // At this point, `signature` has been refined to a + // `Signature` that can be used with the RPC. + const { + value: [status], + } = await rpc.getSignatureStatuses([signature]).send(); + setSignatureStatus(status); +} else { + setError(`${signature} is not a transaction signature`); +} +``` + ### `signBytes()` Given a private `CryptoKey` and a `Uint8Array` of bytes, this method will return the 64-byte Ed25519 signature of that data as a `Uint8Array`. @@ -47,9 +95,22 @@ const data = new Uint8Array([1, 2, 3]); const signature = await signBytes(privateKey, data); ``` +### `signature()` + +This helper combines _asserting_ that a string is an Ed25519 signature with _coercing_ it to the `Signature` type. It's best used with untrusted input. + +```ts +import { signature } from '@solana/keys'; + +const signature = signature(userSuppliedSignature); +const { + value: [status], +} = await rpc.getSignatureStatuses([signature]).send(); +``` + ### `verifySignature()` -Given a public `CryptoKey`, an `Ed25519Signature`, and a `Uint8Array` of bytes, this method will return `true` if the signature was produced by signing the bytes using the private key associated with the public key, and `false` otherwise. +Given a public `CryptoKey`, some `SignatureBytes`, and a `Uint8Array` of data, this method will return `true` if the signature was produced by signing the data using the private key associated with the public key, and `false` otherwise. ```ts import { verifySignature } from '@solana/keys'; diff --git a/packages/keys/package.json b/packages/keys/package.json index d9ec6645e0c7..d02736a00508 100644 --- a/packages/keys/package.json +++ b/packages/keys/package.json @@ -65,7 +65,9 @@ "node": ">=17.4" }, "dependencies": { - "@solana/assertions": "workspace:*" + "@solana/assertions": "workspace:*", + "@solana/codecs-core": "workspace:*", + "@solana/codecs-strings": "workspace:*" }, "devDependencies": { "@solana/eslint-config-solana": "^1.0.2", diff --git a/packages/keys/src/__tests__/coercions-test.ts b/packages/keys/src/__tests__/coercions-test.ts new file mode 100644 index 000000000000..815c4a803554 --- /dev/null +++ b/packages/keys/src/__tests__/coercions-test.ts @@ -0,0 +1,17 @@ +import { Signature, signature } from '../signatures'; + +describe('signature', () => { + it('can coerce to `Signature`', () => { + // Randomly generated + const raw = + '3bwsNoq6EP89sShUAKBeB26aCC3KLGNajRm5wqwr6zRPP3gErZH7erSg3332SVY7Ru6cME43qT35Z7JKpZqCoPaL' as Signature; + const coerced = signature( + '3bwsNoq6EP89sShUAKBeB26aCC3KLGNajRm5wqwr6zRPP3gErZH7erSg3332SVY7Ru6cME43qT35Z7JKpZqCoPaL' + ); + expect(coerced).toBe(raw); + }); + it('throws on invalid `Signature`', () => { + const thisThrows = () => signature('test'); + expect(thisThrows).toThrow('`test` is not a signature'); + }); +}); diff --git a/packages/keys/src/__tests__/signatures-test.ts b/packages/keys/src/__tests__/signatures-test.ts index 3b89c12a88d6..d70a0914998a 100644 --- a/packages/keys/src/__tests__/signatures-test.ts +++ b/packages/keys/src/__tests__/signatures-test.ts @@ -1,11 +1,19 @@ -import { Ed25519Signature, signBytes, verifySignature } from '../signatures'; +import { Encoder } from '@solana/codecs-core'; +import { getBase58Encoder } from '@solana/codecs-strings'; + +import { SignatureBytes, signBytes, verifySignature } from '../signatures'; + +jest.mock('@solana/codecs-strings', () => ({ + ...jest.requireActual('@solana/codecs-strings'), + getBase58Encoder: jest.fn(), +})); const MOCK_DATA = new Uint8Array([1, 2, 3]); const MOCK_DATA_SIGNATURE = new Uint8Array([ 66, 111, 184, 228, 239, 189, 127, 46, 23, 168, 117, 69, 58, 143, 132, 164, 112, 189, 203, 228, 183, 151, 0, 23, 179, 181, 52, 75, 112, 225, 150, 128, 184, 164, 36, 21, 101, 205, 115, 28, 127, 221, 24, 135, 229, 8, 69, 232, 16, 225, 44, 229, 17, 236, 206, 174, 102, 207, 79, 253, 96, 7, 174, 10, -]) as Ed25519Signature; +]) as SignatureBytes; const MOCK_PKCS8_PRIVATE_KEY = // prettier-ignore new Uint8Array([ @@ -48,6 +56,120 @@ const MOCK_PUBLIC_KEY_BYTES = new Uint8Array([ 0xde, 0x31, 0xa1, 0xc9, 0x42, 0x87, 0xcb, 0x43, 0xf0, 0x5f, 0xc9, 0xf2, 0xb5, ]); +// real implementations +const originalBase58Module = jest.requireActual('@solana/codecs-strings'); +const originalGetBase58Encoder = originalBase58Module.getBase58Encoder(); + +describe('assertIsSignature()', () => { + let assertIsSignature: typeof import('../signatures').assertIsSignature; + // Reload `assertIsSignature` before each test to reset memoized state + beforeEach(async () => { + await jest.isolateModulesAsync(async () => { + const base58ModulePromise = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + import('../signatures'); + assertIsSignature = (await base58ModulePromise).assertIsSignature; + }); + }); + + describe('using the real base58 implementation', () => { + beforeEach(() => { + // use real implementation + jest.mocked(getBase58Encoder).mockReturnValue(originalGetBase58Encoder); + }); + + it('throws when supplied a non-base58 string', () => { + expect(() => { + assertIsSignature('not-a-base-58-encoded-string'); + }).toThrow(); + }); + it('throws when the decoded byte array has a length other than 32 bytes', () => { + expect(() => { + assertIsSignature( + // 63 bytes [128, ..., 128] + '1'.repeat(63) + ); + }).toThrow(); + }); + it('does not throw when supplied a base-58 encoded signature', () => { + expect(() => { + // 64 bytes [0, ..., 0] + assertIsSignature('1'.repeat(64)); + + // example signatures + assertIsSignature( + '5HkW5GttYoahVHaujuxEyfyq7RwvoKpc94ko5Fq9GuYdyhejg9cHcqm1MjEvHsjaADRe6hVBqB2E4RQgGgxeA2su' + ); + assertIsSignature( + '2VZm7DkqSKaHxsGiAuVuSkvEbGWf7JrfRdPTw42WKuJC8qw7yQbGL5AE7UxHH3tprgmT9EVbambnK9h3PLpvMvES' + ); + assertIsSignature( + '5sXRtm61WrRGRTjJ6f2anKUWt86Y4V9gWU4WUpue4T4Zh6zuvFoSyaX5LkEtChfqVC8oHdqLo2eUXbhVduThBdfG' + ); + assertIsSignature( + '2Dy6Qai5JyChoP4BKoh9KAYhpD96CUhmEce1GJ8HpV5h8Q4CgUt8KZQzhVNDEQYcjARxYyBNhNjhKUGC2XLZtCCm' + ); + }).not.toThrow(); + }); + it('returns undefined when supplied a base-58 encoded signature', () => { + // 64 bytes [0, ..., 0] + expect(assertIsSignature('1'.repeat(64))).toBeUndefined(); + }); + }); + + describe('using a mock base58 implementation', () => { + const mockEncode = jest.fn(); + beforeEach(() => { + // use mock implementation + mockEncode.mockClear(); + jest.mocked(getBase58Encoder).mockReturnValue({ encode: mockEncode } as unknown as Encoder); + }); + [64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88].forEach( + len => { + it(`attempts to decode input strings of exactly ${len} characters`, () => { + try { + assertIsSignature('1'.repeat(len)); + // eslint-disable-next-line no-empty + } catch {} + expect(mockEncode).toHaveBeenCalledTimes(1); + }); + } + ); + it('does not attempt to decode too-short input strings', () => { + try { + assertIsSignature( + // 63 bytes [0, ..., 0] + '1'.repeat(63) + ); + // eslint-disable-next-line no-empty + } catch {} + expect(mockEncode).not.toHaveBeenCalled(); + }); + it('does not attempt to decode too-long input strings', () => { + try { + assertIsSignature( + // 65 bytes [0, 255, ..., 255] + '167rpwLCuS5DGA8KGZXKsVQ7dnPb9goRLoKfgGbLfQg9WoLUgNY77E2jT11fem3coV9nAkguBACzrU1iyZM4B8roQ' + ); + // eslint-disable-next-line no-empty + } catch {} + expect(mockEncode).not.toHaveBeenCalled(); + }); + it('memoizes getBase58Encoder when called multiple times', () => { + try { + assertIsSignature('1'.repeat(64)); + // eslint-disable-next-line no-empty + } catch {} + try { + assertIsSignature('1'.repeat(64)); + // eslint-disable-next-line no-empty + } catch {} + expect(jest.mocked(getBase58Encoder)).toHaveBeenCalledTimes(1); + }); + }); +}); + describe('sign', () => { it('produces the expected signature given a private key', async () => { expect.assertions(1); @@ -89,19 +211,19 @@ describe('verify', () => { }); it('returns `false` when a bad signature is supplied for a given payload', async () => { expect.assertions(1); - const badSignature = new Uint8Array(Array(64).fill(1)) as Ed25519Signature; + const badSignature = new Uint8Array(Array(64).fill(1)) as SignatureBytes; const result = await verifySignature(mockPublicKey, badSignature, MOCK_DATA); expect(result).toBe(false); }); it('returns `false` when the signature 65 bytes long', async () => { expect.assertions(1); - const badSignature = new Uint8Array([...MOCK_DATA_SIGNATURE, 1]) as Ed25519Signature; + const badSignature = new Uint8Array([...MOCK_DATA_SIGNATURE, 1]) as SignatureBytes; const result = await verifySignature(mockPublicKey, badSignature, MOCK_DATA); expect(result).toBe(false); }); it('returns `false` when the signature 63 bytes long', async () => { expect.assertions(1); - const badSignature = MOCK_DATA_SIGNATURE.slice(0, 63) as Ed25519Signature; + const badSignature = MOCK_DATA_SIGNATURE.slice(0, 63) as SignatureBytes; const result = await verifySignature(mockPublicKey, badSignature, MOCK_DATA); expect(result).toBe(false); }); diff --git a/packages/keys/src/__typetests__/coercions-typetests.ts b/packages/keys/src/__typetests__/coercions-typetests.ts new file mode 100644 index 000000000000..531041db4313 --- /dev/null +++ b/packages/keys/src/__typetests__/coercions-typetests.ts @@ -0,0 +1,3 @@ +import { Signature, signature } from '../signatures'; + +signature('x') satisfies Signature; diff --git a/packages/keys/src/signatures.ts b/packages/keys/src/signatures.ts index fd77319500e9..5ba343e2a046 100644 --- a/packages/keys/src/signatures.ts +++ b/packages/keys/src/signatures.ts @@ -1,14 +1,71 @@ import { assertSigningCapabilityIsAvailable, assertVerificationCapabilityIsAvailable } from '@solana/assertions'; +import { Encoder } from '@solana/codecs-core'; +import { getBase58Encoder } from '@solana/codecs-strings'; -export type Ed25519Signature = Uint8Array & { readonly __brand: unique symbol }; +export type Signature = string & { readonly __brand: unique symbol }; +export type SignatureBytes = Uint8Array & { readonly __brand: unique symbol }; -export async function signBytes(key: CryptoKey, data: Uint8Array): Promise { +let base58Encoder: Encoder | undefined; + +export function assertIsSignature(putativeSignature: string): asserts putativeSignature is Signature { + if (!base58Encoder) base58Encoder = getBase58Encoder(); + + try { + // Fast-path; see if the input string is of an acceptable length. + if ( + // Lowest value (64 bytes of zeroes) + putativeSignature.length < 64 || + // Highest value (64 bytes of 255) + putativeSignature.length > 88 + ) { + throw new Error('Expected input string to decode to a byte array of length 64.'); + } + // Slow-path; actually attempt to decode the input string. + const bytes = base58Encoder.encode(putativeSignature); + const numBytes = bytes.byteLength; + if (numBytes !== 64) { + throw new Error(`Expected input string to decode to a byte array of length 64. Actual length: ${numBytes}`); + } + } catch (e) { + throw new Error(`\`${putativeSignature}\` is not a signature`, { + cause: e, + }); + } +} + +export function isSignature(putativeSignature: string): putativeSignature is Signature { + if (!base58Encoder) base58Encoder = getBase58Encoder(); + + // Fast-path; see if the input string is of an acceptable length. + if ( + // Lowest value (64 bytes of zeroes) + putativeSignature.length < 64 || + // Highest value (64 bytes of 255) + putativeSignature.length > 88 + ) { + return false; + } + // Slow-path; actually attempt to decode the input string. + const bytes = base58Encoder.encode(putativeSignature); + const numBytes = bytes.byteLength; + if (numBytes !== 64) { + return false; + } + return true; +} + +export async function signBytes(key: CryptoKey, data: Uint8Array): Promise { await assertSigningCapabilityIsAvailable(); const signedData = await crypto.subtle.sign('Ed25519', key, data); - return new Uint8Array(signedData) as Ed25519Signature; + return new Uint8Array(signedData) as SignatureBytes; +} + +export function signature(putativeSignature: string): Signature { + assertIsSignature(putativeSignature); + return putativeSignature; } -export async function verifySignature(key: CryptoKey, signature: Ed25519Signature, data: Uint8Array): Promise { +export async function verifySignature(key: CryptoKey, signature: SignatureBytes, data: Uint8Array): Promise { await assertVerificationCapabilityIsAvailable(); return await crypto.subtle.verify('Ed25519', key, signature, data); } diff --git a/packages/keys/tsconfig.json b/packages/keys/tsconfig.json index d04babad0af9..48790bda0522 100644 --- a/packages/keys/tsconfig.json +++ b/packages/keys/tsconfig.json @@ -4,6 +4,6 @@ "extends": "tsconfig/base.json", "include": ["src"], "compilerOptions": { - "lib": ["DOM", "ES2015"] + "lib": ["DOM", "ES2015", "ES2022.Error"] } } diff --git a/packages/library/README.md b/packages/library/README.md index 764c345c30c3..d7998638c560 100644 --- a/packages/library/README.md +++ b/packages/library/README.md @@ -592,7 +592,7 @@ const keyPair: CryptoKeyPair = await generateKeyPair(); const message = new Uint8Array(8).fill(0); const signedMessage = await signBytes(keyPair.privateKey, message); -// ^ Ed25519Signature +// ^ Signature const verified = await verifySignature(keyPair.publicKey, signedMessage, message); ``` diff --git a/packages/library/src/__tests__/airdrop-confirmer-test.ts b/packages/library/src/__tests__/airdrop-confirmer-test.ts index 8745aced5380..bae2a4b881a8 100644 --- a/packages/library/src/__tests__/airdrop-confirmer-test.ts +++ b/packages/library/src/__tests__/airdrop-confirmer-test.ts @@ -1,4 +1,4 @@ -import { TransactionSignature } from '@solana/transactions'; +import { Signature } from '@solana/keys'; import { waitForRecentTransactionConfirmationUntilTimeout } from '../airdrop-confirmer'; @@ -7,7 +7,7 @@ const FOREVER_PROMISE = new Promise(() => { }); describe('waitForRecentTransactionConfirmationUntilTimeout', () => { - const MOCK_SIGNATURE = '4'.repeat(44) as TransactionSignature; + const MOCK_SIGNATURE = '4'.repeat(44) as Signature; let getTimeoutPromise: jest.Mock>; let getRecentSignatureConfirmationPromise: jest.Mock>; beforeEach(() => { diff --git a/packages/library/src/__tests__/airdrop-test.ts b/packages/library/src/__tests__/airdrop-test.ts index f289fab5c826..2468ab7411f6 100644 --- a/packages/library/src/__tests__/airdrop-test.ts +++ b/packages/library/src/__tests__/airdrop-test.ts @@ -1,9 +1,9 @@ import { Address } from '@solana/addresses'; +import { Signature } from '@solana/keys'; import { GetSignatureStatusesApi } from '@solana/rpc-core/dist/types/rpc-methods/getSignatureStatuses'; import { RequestAirdropApi } from '@solana/rpc-core/dist/types/rpc-methods/requestAirdrop'; import { Rpc } from '@solana/rpc-transport/dist/types/json-rpc-types'; import { lamports } from '@solana/rpc-types'; -import { TransactionSignature } from '@solana/transactions'; import { requestAndConfirmAirdrop } from '../airdrop'; @@ -48,7 +48,7 @@ describe('requestAndConfirmAirdrop', () => { it('aborts the `confirmSignatureOnlyTransaction` call when aborted', async () => { expect.assertions(2); const abortController = new AbortController(); - sendAirdropRequest.mockResolvedValue('abc' as TransactionSignature); + sendAirdropRequest.mockResolvedValue('abc' as Signature); requestAndConfirmAirdrop({ abortSignal: abortController.signal, commitment: 'finalized', @@ -72,7 +72,7 @@ describe('requestAndConfirmAirdrop', () => { }); it('passes the expected input to the airdrop request', async () => { expect.assertions(1); - sendAirdropRequest.mockResolvedValue('abc' as TransactionSignature); + sendAirdropRequest.mockResolvedValue('abc' as Signature); requestAndConfirmAirdrop({ abortSignal: new AbortController().signal, commitment: 'finalized', @@ -85,7 +85,7 @@ describe('requestAndConfirmAirdrop', () => { }); it('passes the expected input to the transaction confirmer', async () => { expect.assertions(1); - sendAirdropRequest.mockResolvedValue('abc' as TransactionSignature); + sendAirdropRequest.mockResolvedValue('abc' as Signature); requestAndConfirmAirdrop({ abortSignal: new AbortController().signal, commitment: 'finalized', @@ -98,12 +98,12 @@ describe('requestAndConfirmAirdrop', () => { expect(confirmSignatureOnlyTransaction).toHaveBeenCalledWith({ abortSignal: expect.any(AbortSignal), commitment: 'finalized', - signature: 'abc' as TransactionSignature, + signature: 'abc' as Signature, }); }); it('returns the airdrop transaction signature on success', async () => { expect.assertions(1); - sendAirdropRequest.mockResolvedValue('abc' as TransactionSignature); + sendAirdropRequest.mockResolvedValue('abc' as Signature); confirmSignatureOnlyTransaction.mockResolvedValue(undefined); const airdropPromise = requestAndConfirmAirdrop({ abortSignal: new AbortController().signal, diff --git a/packages/library/src/__tests__/send-transaction-test.ts b/packages/library/src/__tests__/send-transaction-test.ts index 3450ff0f69ea..459475f3dba0 100644 --- a/packages/library/src/__tests__/send-transaction-test.ts +++ b/packages/library/src/__tests__/send-transaction-test.ts @@ -1,4 +1,4 @@ -import { Base58EncodedTransactionSignature } from '@solana/rpc-core/dist/types/rpc-methods/common'; +import { Signature } from '@solana/keys'; import { SendTransactionApi } from '@solana/rpc-core/dist/types/rpc-methods/sendTransaction'; import { Rpc } from '@solana/rpc-transport/dist/types/json-rpc-types'; import { Commitment } from '@solana/rpc-types'; @@ -81,7 +81,7 @@ describe('sendAndConfirmTransaction', () => { preflightCommitment: 'confirmed' as Commitment, skipPreflight: false, } as Parameters[1]; - sendTransaction.mockResolvedValue('abc' as Base58EncodedTransactionSignature); + sendTransaction.mockResolvedValue('abc' as Signature); const abortSignal = new AbortController().signal; sendAndConfirmTransaction({ ...sendTransactionConfig, @@ -238,7 +238,7 @@ describe('sendAndConfirmDurableNonceTransaction', () => { preflightCommitment: 'confirmed' as Commitment, skipPreflight: false, } as Parameters[1]; - sendTransaction.mockResolvedValue('abc' as Base58EncodedTransactionSignature); + sendTransaction.mockResolvedValue('abc' as Signature); const abortSignal = new AbortController().signal; sendAndConfirmDurableNonceTransaction({ ...sendTransactionConfig, diff --git a/packages/library/src/__tests__/transaction-confirmation-strategy-racer-test.ts b/packages/library/src/__tests__/transaction-confirmation-strategy-racer-test.ts index c8e962ae5df6..2f87bfc0051f 100644 --- a/packages/library/src/__tests__/transaction-confirmation-strategy-racer-test.ts +++ b/packages/library/src/__tests__/transaction-confirmation-strategy-racer-test.ts @@ -1,4 +1,4 @@ -import { TransactionSignature } from '@solana/transactions'; +import { Signature } from '@solana/keys'; import { raceStrategies } from '../transaction-confirmation-strategy-racer'; @@ -14,7 +14,7 @@ describe('raceStrategies', () => { expect.assertions(2); const getRecentSignatureConfirmationPromise = jest.fn(); raceStrategies( - 'abc' as TransactionSignature, + 'abc' as Signature, { commitment: 'finalized', getRecentSignatureConfirmationPromise, @@ -36,7 +36,7 @@ describe('raceStrategies', () => { const getRecentSignatureConfirmationPromise = jest.fn().mockReturnValue(FOREVER_PROMISE); const abortController = new AbortController(); raceStrategies( - 'abc' as TransactionSignature, + 'abc' as Signature, { abortSignal: abortController.signal, commitment: 'finalized', @@ -58,7 +58,7 @@ describe('raceStrategies', () => { expect.assertions(2); const getSpecificStrategiesForRace = jest.fn().mockReturnValue([]); raceStrategies( - 'abc' as TransactionSignature, + 'abc' as Signature, { commitment: 'finalized', getRecentSignatureConfirmationPromise: jest.fn(), @@ -78,7 +78,7 @@ describe('raceStrategies', () => { const getSpecificStrategiesForRace = jest.fn().mockReturnValue([]); const abortController = new AbortController(); raceStrategies( - 'abc' as TransactionSignature, + 'abc' as Signature, { abortSignal: abortController.signal, commitment: 'finalized', diff --git a/packages/library/src/__tests__/transaction-confirmation-strategy-signature-test.ts b/packages/library/src/__tests__/transaction-confirmation-strategy-signature-test.ts index 53a8ac092c3a..39dceee94a53 100644 --- a/packages/library/src/__tests__/transaction-confirmation-strategy-signature-test.ts +++ b/packages/library/src/__tests__/transaction-confirmation-strategy-signature-test.ts @@ -1,4 +1,4 @@ -import { TransactionSignature } from '@solana/transactions'; +import { Signature } from '@solana/keys'; import { createRecentSignatureConfirmationPromiseFactory } from '../transaction-confirmation-strategy-recent-signature'; @@ -37,7 +37,7 @@ describe('createSignatureConfirmationPromiseFactory', () => { getSignatureConfirmationPromise({ abortSignal: new AbortController().signal, commitment: 'finalized', - signature: 'abc' as TransactionSignature, + signature: 'abc' as Signature, }); await jest.runAllTimersAsync(); expect(createPendingSubscription).toHaveBeenCalledWith('abc', { @@ -56,7 +56,7 @@ describe('createSignatureConfirmationPromiseFactory', () => { getSignatureConfirmationPromise({ abortSignal: new AbortController().signal, commitment: 'finalized', - signature: 'abc' as TransactionSignature, + signature: 'abc' as Signature, }); await jest.runAllTimersAsync(); expect(getSignatureStatusesMock).not.toHaveBeenCalled(); @@ -79,7 +79,7 @@ describe('createSignatureConfirmationPromiseFactory', () => { const signatureConfirmationPromise = getSignatureConfirmationPromise({ abortSignal: new AbortController().signal, commitment: 'finalized', - signature: 'abc' as TransactionSignature, + signature: 'abc' as Signature, }); await jest.runAllTimersAsync(); await expect(Promise.race([signatureConfirmationPromise, 'pending'])).resolves.toBe('pending'); @@ -93,7 +93,7 @@ describe('createSignatureConfirmationPromiseFactory', () => { const signatureConfirmationPromise = getSignatureConfirmationPromise({ abortSignal: new AbortController().signal, commitment: 'finalized', - signature: 'abc' as TransactionSignature, + signature: 'abc' as Signature, }); await jest.runAllTimersAsync(); await expect(Promise.race([signatureConfirmationPromise, 'pending'])).resolves.toBe('pending'); @@ -106,7 +106,7 @@ describe('createSignatureConfirmationPromiseFactory', () => { const signatureConfirmationPromise = getSignatureConfirmationPromise({ abortSignal: new AbortController().signal, commitment: 'finalized', - signature: 'abc' as TransactionSignature, + signature: 'abc' as Signature, }); await expect(signatureConfirmationPromise).resolves.toBeUndefined(); }); @@ -121,7 +121,7 @@ describe('createSignatureConfirmationPromiseFactory', () => { const signatureConfirmationPromise = getSignatureConfirmationPromise({ abortSignal: new AbortController().signal, commitment: 'finalized', - signature: 'abc' as TransactionSignature, + signature: 'abc' as Signature, }); await expect(signatureConfirmationPromise).resolves.toBeUndefined(); }); @@ -134,7 +134,7 @@ describe('createSignatureConfirmationPromiseFactory', () => { const signatureConfirmationPromise = getSignatureConfirmationPromise({ abortSignal: new AbortController().signal, commitment: 'finalized', - signature: 'abc' as TransactionSignature, + signature: 'abc' as Signature, }); await expect(signatureConfirmationPromise).rejects.toThrow('The transaction with signature `abc` failed.'); }); @@ -144,7 +144,7 @@ describe('createSignatureConfirmationPromiseFactory', () => { getSignatureConfirmationPromise({ abortSignal: abortController.signal, commitment: 'finalized', - signature: 'abc' as TransactionSignature, + signature: 'abc' as Signature, }); await jest.runAllTimersAsync(); expect(getSignatureStatusesMock).toHaveBeenCalledWith({ @@ -161,7 +161,7 @@ describe('createSignatureConfirmationPromiseFactory', () => { getSignatureConfirmationPromise({ abortSignal: abortController.signal, commitment: 'finalized', - signature: 'abc' as TransactionSignature, + signature: 'abc' as Signature, }); expect(createSubscriptionIterable).toHaveBeenCalledWith({ abortSignal: expect.objectContaining({ aborted: false }), diff --git a/packages/library/src/__tests__/transaction-confirmation-test.ts b/packages/library/src/__tests__/transaction-confirmation-test.ts index ab5a1a3bd17f..02e06c5e0a3e 100644 --- a/packages/library/src/__tests__/transaction-confirmation-test.ts +++ b/packages/library/src/__tests__/transaction-confirmation-test.ts @@ -1,7 +1,7 @@ import { Address } from '@solana/addresses'; import { AccountRole, ReadonlySignerAccount, WritableAccount } from '@solana/instructions'; -import { Ed25519Signature } from '@solana/keys'; -import { Blockhash, IDurableNonceTransaction, Nonce, TransactionSignature } from '@solana/transactions'; +import { Signature, SignatureBytes } from '@solana/keys'; +import { Blockhash, IDurableNonceTransaction, Nonce } from '@solana/transactions'; import { waitForDurableNonceTransactionConfirmation, @@ -36,7 +36,7 @@ describe('waitForDurableNonceTransactionConfirmation', () => { ], lifetimeConstraint: { nonce: 'xyz' as Nonce }, signatures: { - ['9'.repeat(44) as Address]: new Uint8Array(new Array(64).fill(0)) as Ed25519Signature, + ['9'.repeat(44) as Address]: new Uint8Array(new Array(64).fill(0)) as SignatureBytes, } as const, } as const; let getNonceInvalidationPromise: jest.Mock>; @@ -122,7 +122,7 @@ describe('waitForDurableNonceTransactionConfirmation', () => { expect(getRecentSignatureConfirmationPromise).toHaveBeenCalledWith({ abortSignal: expect.any(AbortSignal), commitment: 'finalized', - signature: '1111111111111111111111111111111111111111111111111111111111111111' as TransactionSignature, + signature: '1111111111111111111111111111111111111111111111111111111111111111' as Signature, }); }); it('throws when supplied a transaction that has not been signed by the fee payer', async () => { @@ -131,7 +131,7 @@ describe('waitForDurableNonceTransactionConfirmation', () => { ...MOCK_DURABLE_NONCE_TRANSACTION, signatures: { // Signature by someone other than the fee payer. - ['456' as Address]: new Uint8Array(new Array(64).fill(0)) as Ed25519Signature, + ['456' as Address]: new Uint8Array(new Array(64).fill(0)) as SignatureBytes, } as const, }; const commitmentPromise = waitForDurableNonceTransactionConfirmation({ @@ -190,7 +190,7 @@ describe('waitForRecentTransactionConfirmation', () => { feePayer: '9'.repeat(44) as Address, lifetimeConstraint: { blockhash: '4'.repeat(44) as Blockhash, lastValidBlockHeight: 123n }, signatures: { - ['9'.repeat(44) as Address]: new Uint8Array(new Array(64).fill(0)) as Ed25519Signature, + ['9'.repeat(44) as Address]: new Uint8Array(new Array(64).fill(0)) as SignatureBytes, } as const, } as const; let getBlockHeightExceedencePromise: jest.Mock>; @@ -238,7 +238,7 @@ describe('waitForRecentTransactionConfirmation', () => { expect(getRecentSignatureConfirmationPromise).toHaveBeenCalledWith({ abortSignal: expect.any(AbortSignal), commitment: 'finalized', - signature: '1111111111111111111111111111111111111111111111111111111111111111' as TransactionSignature, + signature: '1111111111111111111111111111111111111111111111111111111111111111' as Signature, }); }); it('throws when supplied a transaction that has not been signed by the fee payer', async () => { @@ -247,7 +247,7 @@ describe('waitForRecentTransactionConfirmation', () => { ...MOCK_TRANSACTION, signatures: { // Signature by someone other than the fee payer. - ['456' as Address]: new Uint8Array(new Array(64).fill(0)) as Ed25519Signature, + ['456' as Address]: new Uint8Array(new Array(64).fill(0)) as SignatureBytes, } as const, }; const commitmentPromise = waitForRecentTransactionConfirmation({ diff --git a/packages/library/src/airdrop-confirmer.ts b/packages/library/src/airdrop-confirmer.ts index e418da0fe484..2a940da1ff5d 100644 --- a/packages/library/src/airdrop-confirmer.ts +++ b/packages/library/src/airdrop-confirmer.ts @@ -1,7 +1,7 @@ +import { Signature } from '@solana/keys'; import { GetSignatureStatusesApi } from '@solana/rpc-core/dist/types/rpc-methods/getSignatureStatuses'; import { SignatureNotificationsApi } from '@solana/rpc-core/dist/types/rpc-subscriptions/signature-notifications'; import { Rpc, RpcSubscriptions } from '@solana/rpc-transport/dist/types/json-rpc-types'; -import { TransactionSignature } from '@solana/transactions'; import { BaseTransactionConfirmationStrategyConfig, raceStrategies } from './transaction-confirmation-strategy-racer'; import { createRecentSignatureConfirmationPromiseFactory } from './transaction-confirmation-strategy-recent-signature'; @@ -15,7 +15,7 @@ interface DefaultSignatureOnlyRecentTransactionConfirmerConfig { interface WaitForRecentTransactionWithTimeBasedLifetimeConfirmationConfig extends BaseTransactionConfirmationStrategyConfig { getTimeoutPromise: typeof getTimeoutPromise; - signature: TransactionSignature; + signature: Signature; } /** @deprecated */ diff --git a/packages/library/src/airdrop.ts b/packages/library/src/airdrop.ts index 31f80c5903fc..9acb1acfbbdb 100644 --- a/packages/library/src/airdrop.ts +++ b/packages/library/src/airdrop.ts @@ -1,10 +1,10 @@ import { Address } from '@solana/addresses'; +import { Signature } from '@solana/keys'; import { GetSignatureStatusesApi } from '@solana/rpc-core/dist/types/rpc-methods/getSignatureStatuses'; import { RequestAirdropApi } from '@solana/rpc-core/dist/types/rpc-methods/requestAirdrop'; import { SignatureNotificationsApi } from '@solana/rpc-core/dist/types/rpc-subscriptions/signature-notifications'; import { Rpc, RpcSubscriptions } from '@solana/rpc-transport/dist/types/json-rpc-types'; import { Commitment, LamportsUnsafeBeyond2Pow53Minus1 } from '@solana/rpc-types'; -import { TransactionSignature } from '@solana/transactions'; import { createDefaultSignatureOnlyRecentTransactionConfirmer } from './airdrop-confirmer'; @@ -43,10 +43,10 @@ export async function requestAndConfirmAirdrop({ lamports: LamportsUnsafeBeyond2Pow53Minus1; recipientAddress: Address; rpc: Rpc; -}>): Promise { - const airdropTransactionSignature = (await rpc +}>): Promise { + const airdropTransactionSignature = await rpc .requestAirdrop(recipientAddress, lamports, { commitment }) - .send({ abortSignal })) as unknown as TransactionSignature; // FIXME(#1709) + .send({ abortSignal }); await confirmSignatureOnlyTransaction({ abortSignal, commitment, diff --git a/packages/library/src/send-transaction.ts b/packages/library/src/send-transaction.ts index 6f37f9526b6b..d9b4e133562b 100644 --- a/packages/library/src/send-transaction.ts +++ b/packages/library/src/send-transaction.ts @@ -1,3 +1,4 @@ +import { Signature } from '@solana/keys'; import { SendTransactionApi } from '@solana/rpc-core/dist/types/rpc-methods/sendTransaction'; import { Rpc } from '@solana/rpc-transport/dist/types/json-rpc-types'; import { Commitment, commitmentComparator } from '@solana/rpc-types'; @@ -8,7 +9,6 @@ import { IFullySignedTransaction, ITransactionWithBlockhashLifetime, ITransactionWithFeePayer, - TransactionSignature, } from '@solana/transactions'; import { @@ -84,14 +84,14 @@ async function sendTransaction_INTERNAL({ rpc, transaction, ...sendTransactionConfig -}: SendTransactionInternalConfig): Promise { +}: SendTransactionInternalConfig): Promise { const base64EncodedWireTransaction = getBase64EncodedWireTransaction(transaction); - return (await rpc + return await rpc .sendTransaction(base64EncodedWireTransaction, { ...getSendTransactionConfigWithAdjustedPreflightCommitment(commitment, sendTransactionConfig), encoding: 'base64', }) - .send({ abortSignal })) as unknown as TransactionSignature; // FIXME(#1709) + .send({ abortSignal }); } export function createDefaultDurableNonceTransactionSender({ @@ -149,7 +149,7 @@ export async function sendAndConfirmDurableNonceTransaction({ rpc, transaction, ...sendTransactionConfig -}: SendAndConfirmDurableNonceTransactionConfig): Promise { +}: SendAndConfirmDurableNonceTransactionConfig): Promise { const transactionSignature = await sendTransaction_INTERNAL({ ...sendTransactionConfig, abortSignal, @@ -172,7 +172,7 @@ export async function sendAndConfirmTransaction({ rpc, transaction, ...sendTransactionConfig -}: SendAndConfirmTransactionWithBlockhashLifetimeConfig): Promise { +}: SendAndConfirmTransactionWithBlockhashLifetimeConfig): Promise { const transactionSignature = await sendTransaction_INTERNAL({ ...sendTransactionConfig, abortSignal, diff --git a/packages/library/src/transaction-confirmation-strategy-racer.ts b/packages/library/src/transaction-confirmation-strategy-racer.ts index 55bbe86313a4..af41d3d50ca7 100644 --- a/packages/library/src/transaction-confirmation-strategy-racer.ts +++ b/packages/library/src/transaction-confirmation-strategy-racer.ts @@ -1,5 +1,5 @@ +import { Signature } from '@solana/keys'; import { Commitment } from '@solana/rpc-types'; -import { TransactionSignature } from '@solana/transactions'; import { createRecentSignatureConfirmationPromiseFactory } from './transaction-confirmation-strategy-recent-signature'; @@ -12,7 +12,7 @@ export interface BaseTransactionConfirmationStrategyConfig { type WithNonNullableAbortSignal = Omit & Readonly<{ abortSignal: AbortSignal }>; export async function raceStrategies( - signature: TransactionSignature, + signature: Signature, config: TConfig, getSpecificStrategiesForRace: (config: WithNonNullableAbortSignal) => readonly Promise[] ) { diff --git a/packages/library/src/transaction-confirmation-strategy-recent-signature.ts b/packages/library/src/transaction-confirmation-strategy-recent-signature.ts index 4266acd2fdd2..9b4b9bc06863 100644 --- a/packages/library/src/transaction-confirmation-strategy-recent-signature.ts +++ b/packages/library/src/transaction-confirmation-strategy-recent-signature.ts @@ -1,13 +1,13 @@ +import { Signature } from '@solana/keys'; import { GetSignatureStatusesApi } from '@solana/rpc-core/dist/types/rpc-methods/getSignatureStatuses'; import { SignatureNotificationsApi } from '@solana/rpc-core/dist/types/rpc-subscriptions/signature-notifications'; import { Rpc, RpcSubscriptions } from '@solana/rpc-transport/dist/types/json-rpc-types'; import { Commitment, commitmentComparator } from '@solana/rpc-types'; -import { TransactionSignature } from '@solana/transactions'; type GetRecentSignatureConfirmationPromiseFn = (config: { abortSignal: AbortSignal; commitment: Commitment; - signature: TransactionSignature; + signature: Signature; }) => Promise; export function createRecentSignatureConfirmationPromiseFactory( diff --git a/packages/rpc-core/package.json b/packages/rpc-core/package.json index cd543ea67fc4..cdeb50dda2fb 100644 --- a/packages/rpc-core/package.json +++ b/packages/rpc-core/package.json @@ -66,6 +66,7 @@ "@solana/codecs-core": "workspace:*", "@solana/codecs-strings": "workspace:*", "@solana/eslint-config-solana": "^1.0.2", + "@solana/keys": "workspace:*", "@solana/rpc-transport": "workspace:*", "@solana/rpc-types": "workspace:*", "@solana/transactions": "workspace:*", diff --git a/packages/rpc-core/src/rpc-methods/__tests__/get-signature-statuses-test.ts b/packages/rpc-core/src/rpc-methods/__tests__/get-signature-statuses-test.ts index 019870f77e38..3b6f8384df59 100644 --- a/packages/rpc-core/src/rpc-methods/__tests__/get-signature-statuses-test.ts +++ b/packages/rpc-core/src/rpc-methods/__tests__/get-signature-statuses-test.ts @@ -1,7 +1,7 @@ +import { Signature } from '@solana/keys'; import { createHttpTransport, createJsonRpc } from '@solana/rpc-transport'; import { SolanaJsonRpcErrorCode } from '@solana/rpc-transport/dist/types/json-rpc-errors'; import type { Rpc } from '@solana/rpc-transport/dist/types/json-rpc-types'; -import { TransactionSignature } from '@solana/transactions'; import fetchMock from 'jest-fetch-mock-fork'; import { createSolanaRpcApi, SolanaRpcMethods } from '../index'; @@ -51,9 +51,7 @@ describe('getSignatureStatuses', () => { describe('when called with an invalid transaction signature', () => { it('throws an error', async () => { expect.assertions(1); - const signatureStatusPromise = rpc - .getSignatureStatuses(['invalid_signature' as TransactionSignature]) - .send(); + const signatureStatusPromise = rpc.getSignatureStatuses(['invalid_signature' as Signature]).send(); await expect(signatureStatusPromise).rejects.toMatchObject({ code: -32602 satisfies (typeof SolanaJsonRpcErrorCode)['JSON_RPC_INVALID_PARAMS'], message: expect.any(String), @@ -68,7 +66,7 @@ describe('getSignatureStatuses', () => { const signatureStatusPromise = rpc .getSignatureStatuses([ // Randomly generated - '4Vx3PAb665jCLRpbpgKshZuwKP6TUgoSDDAbKEsyvkKhwrNDT6CE5d7MT1vEPkgEo1cmr7zsM8h724wRnjyCAoR3' as TransactionSignature, + '4Vx3PAb665jCLRpbpgKshZuwKP6TUgoSDDAbKEsyvkKhwrNDT6CE5d7MT1vEPkgEo1cmr7zsM8h724wRnjyCAoR3' as Signature, ]) .send(); await expect(signatureStatusPromise).resolves.toStrictEqual({ diff --git a/packages/rpc-core/src/rpc-methods/common.ts b/packages/rpc-core/src/rpc-methods/common.ts index 82a3fb221d01..e62faae477ac 100644 --- a/packages/rpc-core/src/rpc-methods/common.ts +++ b/packages/rpc-core/src/rpc-methods/common.ts @@ -49,8 +49,6 @@ export type Base58EncodedDataResponse = [Base58EncodedBytes, 'base58']; export type Base64EncodedDataResponse = [Base64EncodedBytes, 'base64']; export type Base64EncodedZStdCompressedDataResponse = [Base64EncodedZStdCompressedBytes, 'base64+zstd']; -export type Base58EncodedTransactionSignature = string & { readonly __brand: unique symbol }; - export type AccountInfoBase = Readonly<{ /** indicates if the account contains a program (and is strictly read-only) */ executable: boolean; diff --git a/packages/rpc-core/src/rpc-methods/getSignatureStatuses.ts b/packages/rpc-core/src/rpc-methods/getSignatureStatuses.ts index e3e9fe0d3722..e5e1947c6a7c 100644 --- a/packages/rpc-core/src/rpc-methods/getSignatureStatuses.ts +++ b/packages/rpc-core/src/rpc-methods/getSignatureStatuses.ts @@ -1,5 +1,5 @@ +import { Signature } from '@solana/keys'; import { Commitment } from '@solana/rpc-types'; -import { TransactionSignature } from '@solana/transactions'; import { TransactionError } from '../transaction-error'; import { RpcResponse, Slot, U64UnsafeBeyond2Pow53Minus1 } from './common'; @@ -54,7 +54,7 @@ export interface GetSignatureStatusesApi { * An array of transaction signatures to confirm, * as base-58 encoded strings (up to a maximum of 256) */ - signatures: TransactionSignature[], + signatures: Signature[], config?: Readonly<{ /** * if `true` - a Solana node will search its ledger cache for any diff --git a/packages/rpc-core/src/rpc-methods/getSignaturesForAddress.ts b/packages/rpc-core/src/rpc-methods/getSignaturesForAddress.ts index 63794e2c4e0a..88a70fc1b1ba 100644 --- a/packages/rpc-core/src/rpc-methods/getSignaturesForAddress.ts +++ b/packages/rpc-core/src/rpc-methods/getSignaturesForAddress.ts @@ -1,13 +1,13 @@ import { Address } from '@solana/addresses'; +import { Signature } from '@solana/keys'; import { Commitment, UnixTimestamp } from '@solana/rpc-types'; -import { TransactionSignature } from '@solana/transactions'; import { TransactionError } from '../transaction-error'; import { RpcResponse, Slot } from './common'; type GetSignaturesForAddressTransaction = RpcResponse<{ /** transaction signature as base-58 encoded string */ - signature: TransactionSignature; + signature: Signature; /** The slot that contains the block with the transaction */ slot: Slot; /** Error if transaction failed, null if transaction succeeded. */ @@ -31,9 +31,9 @@ type GetSignaturesForAddressConfig = Readonly<{ /** maximum transaction signatures to return (between 1 and 1,000). Default: 1000 */ limit?: number; /** start searching backwards from this transaction signature. If not provided the search starts from the top of the highest max confirmed block. */ - before?: TransactionSignature; + before?: Signature; /** search until this transaction signature, if found before limit reached */ - until?: TransactionSignature; + until?: Signature; }>; export interface GetSignaturesForAddressApi { diff --git a/packages/rpc-core/src/rpc-methods/getTransaction.ts b/packages/rpc-core/src/rpc-methods/getTransaction.ts index ec8ddb2858ac..33b728040b55 100644 --- a/packages/rpc-core/src/rpc-methods/getTransaction.ts +++ b/packages/rpc-core/src/rpc-methods/getTransaction.ts @@ -1,7 +1,7 @@ import { Address } from '@solana/addresses'; +import { Signature } from '@solana/keys'; import { Commitment, LamportsUnsafeBeyond2Pow53Minus1, UnixTimestamp } from '@solana/rpc-types'; import { Blockhash, TransactionVersion } from '@solana/transactions'; -import { TransactionSignature } from '@solana/transactions'; import { TransactionError } from '../transaction-error'; import { @@ -159,7 +159,7 @@ export interface GetTransactionApi { * Returns transaction details for a confirmed transaction */ getTransaction( - signature: TransactionSignature, + signature: Signature, config: GetTransactionCommonConfig & Readonly<{ encoding: 'jsonParsed'; @@ -177,7 +177,7 @@ export interface GetTransactionApi { }) | null; getTransaction( - signature: TransactionSignature, + signature: Signature, config: GetTransactionCommonConfig & Readonly<{ encoding: 'base64'; @@ -198,7 +198,7 @@ export interface GetTransactionApi { }) | null; getTransaction( - signature: TransactionSignature, + signature: Signature, config: GetTransactionCommonConfig & Readonly<{ encoding: 'base58'; @@ -219,7 +219,7 @@ export interface GetTransactionApi { }) | null; getTransaction( - signature: TransactionSignature, + signature: Signature, config?: GetTransactionCommonConfig & Readonly<{ encoding?: 'json'; diff --git a/packages/rpc-core/src/rpc-methods/requestAirdrop.ts b/packages/rpc-core/src/rpc-methods/requestAirdrop.ts index 809c3cd7e5b8..8df4635b3d65 100644 --- a/packages/rpc-core/src/rpc-methods/requestAirdrop.ts +++ b/packages/rpc-core/src/rpc-methods/requestAirdrop.ts @@ -1,13 +1,12 @@ import { Address } from '@solana/addresses'; +import { Signature } from '@solana/keys'; import { Commitment, LamportsUnsafeBeyond2Pow53Minus1 } from '@solana/rpc-types'; -import { Base58EncodedTransactionSignature } from './common'; - type RequestAirdropConfig = Readonly<{ commitment?: Commitment; }>; -type RequestAirdropResponse = Base58EncodedTransactionSignature; +type RequestAirdropResponse = Signature; export interface RequestAirdropApi { /** diff --git a/packages/rpc-core/src/rpc-methods/sendTransaction.ts b/packages/rpc-core/src/rpc-methods/sendTransaction.ts index 2ceda7ef73be..484e32f68141 100644 --- a/packages/rpc-core/src/rpc-methods/sendTransaction.ts +++ b/packages/rpc-core/src/rpc-methods/sendTransaction.ts @@ -1,7 +1,8 @@ +import { Signature } from '@solana/keys'; import { Commitment } from '@solana/rpc-types'; import { Base64EncodedWireTransaction } from '@solana/transactions'; -import { Base58EncodedTransactionSignature, Slot } from './common'; +import { Slot } from './common'; type SendTransactionConfig = Readonly<{ skipPreflight?: boolean; @@ -10,7 +11,7 @@ type SendTransactionConfig = Readonly<{ minContextSlot?: Slot; }>; -type SendTransactionResponse = Base58EncodedTransactionSignature; +type SendTransactionResponse = Signature; export interface SendTransactionApi { /** @deprecated Set `encoding` to `'base64'` when calling this method */ diff --git a/packages/rpc-core/src/rpc-subscriptions/__typetests__/logs-notifications-type-test.ts b/packages/rpc-core/src/rpc-subscriptions/__typetests__/logs-notifications-type-test.ts index 9499c4e3674f..11b57a0ec4b7 100644 --- a/packages/rpc-core/src/rpc-subscriptions/__typetests__/logs-notifications-type-test.ts +++ b/packages/rpc-core/src/rpc-subscriptions/__typetests__/logs-notifications-type-test.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ import { Address } from '@solana/addresses'; +import { Signature } from '@solana/keys'; import { PendingRpcSubscription, RpcSubscriptions } from '@solana/rpc-transport/dist/types/json-rpc-types'; -import { TransactionSignature } from '@solana/transactions'; import { RpcResponse } from '../../rpc-methods/common'; import { TransactionError } from '../../transaction-error'; @@ -15,7 +15,7 @@ async () => { Readonly<{ err: TransactionError | null; logs: readonly string[] | null; - signature: TransactionSignature; + signature: Signature; }> >; rpcSubscriptions.logsNotifications('all') satisfies PendingRpcSubscription; diff --git a/packages/rpc-core/src/rpc-subscriptions/__typetests__/signature-notifications-type-test.ts b/packages/rpc-core/src/rpc-subscriptions/__typetests__/signature-notifications-type-test.ts index 3346cd4d55c0..69076ed5a13b 100644 --- a/packages/rpc-core/src/rpc-subscriptions/__typetests__/signature-notifications-type-test.ts +++ b/packages/rpc-core/src/rpc-subscriptions/__typetests__/signature-notifications-type-test.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ +import { Signature } from '@solana/keys'; import { PendingRpcSubscription, RpcSubscriptions } from '@solana/rpc-transport/dist/types/json-rpc-types'; -import { TransactionSignature } from '@solana/transactions'; import { RpcResponse } from '../../rpc-methods/common'; import { TransactionError } from '../../transaction-error'; @@ -18,29 +18,29 @@ async () => { >; rpcSubscriptions.signatureNotifications( - 'xxxxx' as TransactionSignature + 'xxxxx' as Signature ) satisfies PendingRpcSubscription; rpcSubscriptions - .signatureNotifications('xxxxx' as TransactionSignature) + .signatureNotifications('xxxxx' as Signature) .subscribe({ abortSignal: new AbortController().signal }) satisfies Promise< AsyncIterable >; - rpcSubscriptions.signatureNotifications('xxxxx' as TransactionSignature, { + rpcSubscriptions.signatureNotifications('xxxxx' as Signature, { commitment: 'confirmed', }) satisfies PendingRpcSubscription; rpcSubscriptions - .signatureNotifications('xxxxx' as TransactionSignature, { commitment: 'confirmed' }) + .signatureNotifications('xxxxx' as Signature, { commitment: 'confirmed' }) .subscribe({ abortSignal: new AbortController().signal }) satisfies Promise< AsyncIterable >; - rpcSubscriptions.signatureNotifications('xxxxx' as TransactionSignature, { + rpcSubscriptions.signatureNotifications('xxxxx' as Signature, { commitment: 'confirmed', enableReceivedNotification: false, }) satisfies PendingRpcSubscription; rpcSubscriptions - .signatureNotifications('xxxxx' as TransactionSignature, { + .signatureNotifications('xxxxx' as Signature, { commitment: 'confirmed', enableReceivedNotification: false, }) @@ -48,25 +48,25 @@ async () => { AsyncIterable >; - rpcSubscriptions.signatureNotifications('xxxxx' as TransactionSignature, { + rpcSubscriptions.signatureNotifications('xxxxx' as Signature, { commitment: 'confirmed', enableReceivedNotification: true, }) satisfies PendingRpcSubscription; rpcSubscriptions - .signatureNotifications('xxxxx' as TransactionSignature, { + .signatureNotifications('xxxxx' as Signature, { commitment: 'confirmed', enableReceivedNotification: true, }) .subscribe({ abortSignal: new AbortController().signal }) satisfies Promise< AsyncIterable >; - rpcSubscriptions.signatureNotifications('xxxxx' as TransactionSignature, { + rpcSubscriptions.signatureNotifications('xxxxx' as Signature, { commitment: 'confirmed', enableReceivedNotification: true, // @ts-expect-error Should have both notification types }) satisfies PendingRpcSubscription; rpcSubscriptions - .signatureNotifications('xxxxx' as TransactionSignature, { + .signatureNotifications('xxxxx' as Signature, { commitment: 'confirmed', enableReceivedNotification: true, }) diff --git a/packages/rpc-core/src/rpc-subscriptions/__typetests__/vote-notifications-type-test.ts b/packages/rpc-core/src/rpc-subscriptions/__typetests__/vote-notifications-type-test.ts index 8991b61d3d67..93a65ba3d39e 100644 --- a/packages/rpc-core/src/rpc-subscriptions/__typetests__/vote-notifications-type-test.ts +++ b/packages/rpc-core/src/rpc-subscriptions/__typetests__/vote-notifications-type-test.ts @@ -1,9 +1,10 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ import { Address } from '@solana/addresses'; +import { Signature } from '@solana/keys'; import { PendingRpcSubscription, RpcSubscriptions } from '@solana/rpc-transport/dist/types/json-rpc-types'; import { UnixTimestamp } from '@solana/rpc-types'; -import { Blockhash, TransactionSignature } from '@solana/transactions'; +import { Blockhash } from '@solana/transactions'; import { Slot } from '../../rpc-methods/common'; import { VoteNotificationsApi } from '../vote-notifications'; @@ -13,7 +14,7 @@ async () => { type VoteNotificationsApiNotification = Readonly<{ hash: Blockhash; - signature: TransactionSignature; + signature: Signature; slots: readonly Slot[]; timestamp: UnixTimestamp | null; votePubkey: Address; diff --git a/packages/rpc-core/src/rpc-subscriptions/logs-notifications.ts b/packages/rpc-core/src/rpc-subscriptions/logs-notifications.ts index 620ff0a0ea73..7b7df613a38d 100644 --- a/packages/rpc-core/src/rpc-subscriptions/logs-notifications.ts +++ b/packages/rpc-core/src/rpc-subscriptions/logs-notifications.ts @@ -1,6 +1,6 @@ import { Address } from '@solana/addresses'; +import { Signature } from '@solana/keys'; import { Commitment } from '@solana/rpc-types'; -import { TransactionSignature } from '@solana/transactions'; import { RpcResponse } from '../rpc-methods/common'; import { TransactionError } from '../transaction-error'; @@ -14,7 +14,7 @@ type LogsNotificationsApiNotification = RpcResponse< // (for example due to an invalid blockhash or signature verification failure) logs: readonly string[] | null; // The transaction signature base58 encoded - signature: TransactionSignature; + signature: Signature; }> >; diff --git a/packages/rpc-core/src/rpc-subscriptions/signature-notifications.ts b/packages/rpc-core/src/rpc-subscriptions/signature-notifications.ts index 33b0f2be6df2..f56de8fba0db 100644 --- a/packages/rpc-core/src/rpc-subscriptions/signature-notifications.ts +++ b/packages/rpc-core/src/rpc-subscriptions/signature-notifications.ts @@ -1,5 +1,5 @@ +import { Signature } from '@solana/keys'; import { Commitment } from '@solana/rpc-types'; -import { TransactionSignature } from '@solana/transactions'; import { RpcResponse } from '../rpc-methods/common'; import { TransactionError } from '../transaction-error'; @@ -25,7 +25,7 @@ export interface SignatureNotificationsApi { */ signatureNotifications( // Transaction Signature, as base-58 encoded string - signature: TransactionSignature, + signature: Signature, config: SignatureNotificationsApiConfigBase & Readonly<{ // Whether or not to subscribe for notifications when signatures are received @@ -34,7 +34,7 @@ export interface SignatureNotificationsApi { }> ): SignatureNotificationsApiNotificationReceived | SignatureNotificationsApiNotificationProcessed; signatureNotifications( - signature: TransactionSignature, + signature: Signature, config?: SignatureNotificationsApiConfigBase & Readonly<{ enableReceivedNotification?: false; diff --git a/packages/rpc-core/src/rpc-subscriptions/vote-notifications.ts b/packages/rpc-core/src/rpc-subscriptions/vote-notifications.ts index 49338c20e4d6..137c16261263 100644 --- a/packages/rpc-core/src/rpc-subscriptions/vote-notifications.ts +++ b/packages/rpc-core/src/rpc-subscriptions/vote-notifications.ts @@ -1,13 +1,13 @@ import { Address } from '@solana/addresses'; +import { Signature } from '@solana/keys'; import { UnixTimestamp } from '@solana/rpc-types'; import { Blockhash } from '@solana/transactions'; -import { TransactionSignature } from '@solana/transactions'; import { Slot } from '../rpc-methods/common'; type VoteNotificationsApiNotification = Readonly<{ hash: Blockhash; - signature: TransactionSignature; + signature: Signature; slots: readonly Slot[]; timestamp: UnixTimestamp | null; votePubkey: Address; diff --git a/packages/rpc-graphql/package.json b/packages/rpc-graphql/package.json index cdc27a431cc2..11cb3dbdd871 100644 --- a/packages/rpc-graphql/package.json +++ b/packages/rpc-graphql/package.json @@ -67,6 +67,7 @@ "devDependencies": { "@solana/addresses": "workspace:*", "@solana/eslint-config-solana": "^1.0.2", + "@solana/keys": "workspace:*", "@solana/rpc-core": "workspace:*", "@solana/rpc-types": "workspace:*", "@solana/rpc-transport": "workspace:*", diff --git a/packages/rpc-graphql/src/schema/transaction/query.ts b/packages/rpc-graphql/src/schema/transaction/query.ts index 9de2a687148c..bdbc512f4682 100644 --- a/packages/rpc-graphql/src/schema/transaction/query.ts +++ b/packages/rpc-graphql/src/schema/transaction/query.ts @@ -1,5 +1,6 @@ +import { Signature } from '@solana/keys'; import { Commitment } from '@solana/rpc-types'; -import { TransactionSignature, TransactionVersion } from '@solana/transactions'; +import { TransactionVersion } from '@solana/transactions'; import { RpcGraphQLContext } from '../../context'; import { commitmentInputType, maxSupportedTransactionVersionInputType, transactionEncodingInputType } from '../inputs'; @@ -7,7 +8,7 @@ import { nonNull, string, type } from '../picks'; import { transactionInterface } from './types'; export type TransactionQueryArgs = { - signature: TransactionSignature; + signature: Signature; commitment?: Commitment; encoding?: 'base58' | 'base64' | 'json' | 'jsonParsed'; maxSupportedTransactionVersion?: Exclude; diff --git a/packages/rpc-types/src/__tests__/coercions-test.ts b/packages/rpc-types/src/__tests__/coercions-test.ts index 9f1e3cf80c40..fbaa89dbef37 100644 --- a/packages/rpc-types/src/__tests__/coercions-test.ts +++ b/packages/rpc-types/src/__tests__/coercions-test.ts @@ -1,5 +1,3 @@ -import { TransactionSignature, transactionSignature } from '@solana/transactions'; - import { lamports, LamportsUnsafeBeyond2Pow53Minus1 } from '../lamports'; import { StringifiedBigInt, stringifiedBigInt } from '../stringified-bigint'; import { StringifiedNumber, stringifiedNumber } from '../stringified-number'; @@ -39,21 +37,6 @@ describe('coercions', () => { expect(thisThrows).toThrow('`test` cannot be parsed as a Number'); }); }); - describe('transactionSignature', () => { - it('can coerce to `TransactionSignature`', () => { - // Randomly generated - const raw = - '3bwsNoq6EP89sShUAKBeB26aCC3KLGNajRm5wqwr6zRPP3gErZH7erSg3332SVY7Ru6cME43qT35Z7JKpZqCoPaL' as TransactionSignature; - const coerced = transactionSignature( - '3bwsNoq6EP89sShUAKBeB26aCC3KLGNajRm5wqwr6zRPP3gErZH7erSg3332SVY7Ru6cME43qT35Z7JKpZqCoPaL' - ); - expect(coerced).toBe(raw); - }); - it('throws on invalid `TransactionSignature`', () => { - const thisThrows = () => transactionSignature('test'); - expect(thisThrows).toThrow('`test` is not a transaction signature'); - }); - }); describe('unixTimestamp', () => { it('can coerce to `UnixTimestamp`', () => { const raw = 1234 as UnixTimestamp; diff --git a/packages/rpc-types/src/__typetests__/coercions-typetests.ts b/packages/rpc-types/src/__typetests__/coercions-typetests.ts index 11a32c8c2e65..4c1c907970e4 100644 --- a/packages/rpc-types/src/__typetests__/coercions-typetests.ts +++ b/packages/rpc-types/src/__typetests__/coercions-typetests.ts @@ -1,5 +1,3 @@ -import { TransactionSignature, transactionSignature } from '@solana/transactions'; - import { lamports, LamportsUnsafeBeyond2Pow53Minus1 } from '../lamports'; import { StringifiedBigInt, stringifiedBigInt } from '../stringified-bigint'; import { StringifiedNumber, stringifiedNumber } from '../stringified-number'; @@ -8,5 +6,4 @@ import { UnixTimestamp, unixTimestamp } from '../unix-timestamp'; lamports(50_000_000_000_000n) satisfies LamportsUnsafeBeyond2Pow53Minus1; stringifiedBigInt('50_000_000_000_000') satisfies StringifiedBigInt; stringifiedNumber('50_000_000_000_000') satisfies StringifiedNumber; -transactionSignature('x') satisfies TransactionSignature; unixTimestamp(0) satisfies UnixTimestamp; diff --git a/packages/transactions/README.md b/packages/transactions/README.md index 75910f903f21..aad53ede9d31 100644 --- a/packages/transactions/README.md +++ b/packages/transactions/README.md @@ -244,21 +244,11 @@ This type represents a transaction that is signed by all of its required signers Expect any function that modifies a transaction (eg. `setTransactionFeePayer`, `appendTransactionInstruction`, et cetera) to delete a transaction's `signatures` property and unset this type. -#### `TransactionSignature` - -This type represents the base58-encoded signature of a transactions fee payer. This value uniquely identifies the transaction on the network. - ### Functions -#### `assertIsTransactionSignature()` - -From time to time you might acquire a string that you expect to be the base58-encoded signature of a transaction, from an untrusted network API or user input. To assert that such an arbitrary string is in fact a transaction signature, use the `assertIsTransactionSignature` function. - -See [`assertIsBlockhash()`](#assertisblockhash) for an example of how to use an assertion function. - #### `getSignatureFromTransaction()` -Given a transaction signed by its fee payer, this method will return the `TransactionSignature` that uniquely identifies it. This string can be used to look up transactions at a later date, for example on a Solana block explorer. +Given a transaction signed by its fee payer, this method will return the `Signature` that uniquely identifies it. This string can be used to look up transactions at a later date, for example on a Solana block explorer. ```ts import { getSignatureFromTransaction } from '@solana/transactions'; @@ -267,25 +257,6 @@ const signature = getSignatureFromTransaction(tx); console.debug(`Inspect this transaction at https://explorer.solana.com/tx/${signature}`); ``` -#### `isTransactionSignature()` - -This is a type guard that accepts a string as input. It will both return `true` if the string conforms to the `TransactionSignature` type and will refine the type for use in your program. - -```ts -import { isTransactionSignature } from '@solana/transactions'; - -if (isTransactionSignature(signature)) { - // At this point, `signature` has been refined to a - // `TransactionSignature` that can be used with the RPC. - const { - value: [status], - } = await rpc.getSignatureStatuses([signature]).send(); - setSignatureStatus(status); -} else { - setError(`${signature} is not a transaction signature`); -} -``` - #### `signTransaction()` Given an array of `CryptoKey` objects which are private keys pertaining to addresses that are required to sign a transaction, this method will return a new signed transaction having the same type as the one supplied plus the `ITransactionWithSignatures` type. @@ -297,19 +268,6 @@ import { signTransaction } from '@solana/transactions'; const signedTransaction = await signTransaction([myPrivateKey], tx); ``` -#### `transactionSignature()` - -This helper combines _asserting_ that a string is a transaction signature with _coercing_ it to the `TransactionSignature` type. It's best used with untrusted input. - -```ts -import { transactionSignature } from '@solana/transactions'; - -const signature = transactionSignature(userSuppliedSignature); -const { - value: [status], -} = await rpc.getSignatureStatuses([signature]).send(); -``` - ## Serializing transactions Before sending a transaction to be landed on the network, you must serialize it in a particular way. You can use these types and functions to serialize a signed transaction into a binary format suitable for transit over the wire. diff --git a/packages/transactions/src/__tests__/decompile-transaction-test.ts b/packages/transactions/src/__tests__/decompile-transaction-test.ts index cda7db3673ae..e9f0815d0e7f 100644 --- a/packages/transactions/src/__tests__/decompile-transaction-test.ts +++ b/packages/transactions/src/__tests__/decompile-transaction-test.ts @@ -1,6 +1,6 @@ import { Address } from '@solana/addresses'; import { AccountRole, IInstruction } from '@solana/instructions'; -import { Ed25519Signature } from '@solana/keys'; +import { SignatureBytes } from '@solana/keys'; import { decompileTransaction } from '../decompile-transaction'; import { Nonce } from '../durable-nonce'; @@ -9,7 +9,7 @@ import { ITransactionWithSignatures } from '../signatures'; type CompiledTransaction = Readonly<{ compiledMessage: CompiledMessage; - signatures: Ed25519Signature[]; + signatures: SignatureBytes[]; }>; describe('decompileTransaction', () => { @@ -193,7 +193,7 @@ describe('decompileTransaction', () => { }); it('converts a transaction with a single signer', () => { - const feePayerSignature = new Uint8Array(Array(64).fill(1)) as Ed25519Signature; + const feePayerSignature = new Uint8Array(Array(64).fill(1)) as SignatureBytes; const compiledTransaction: CompiledTransaction = { compiledMessage: { @@ -212,18 +212,18 @@ describe('decompileTransaction', () => { const transaction = decompileTransaction(compiledTransaction) as ITransactionWithSignatures; expect(transaction.signatures).toStrictEqual({ - [feePayer]: feePayerSignature as Ed25519Signature, + [feePayer]: feePayerSignature as SignatureBytes, }); }); it('converts a transaction with multiple signers', () => { - const feePayerSignature = new Uint8Array(Array(64).fill(1)) as Ed25519Signature; + const feePayerSignature = new Uint8Array(Array(64).fill(1)) as SignatureBytes; const otherSigner1Address = '3LeBzRE9Yna5zi9R8vdT3MiNQYuEp4gJgVyhhwmqfCtd' as Address; - const otherSigner1Signature = new Uint8Array(Array(64).fill(2)) as Ed25519Signature; + const otherSigner1Signature = new Uint8Array(Array(64).fill(2)) as SignatureBytes; const otherSigner2Address = '8kud9bpNvfemXYdTFjs5cZ8fZinBkx8JAnhVmRwJZk5e' as Address; - const otherSigner2Signature = new Uint8Array(Array(64).fill(3)) as Ed25519Signature; + const otherSigner2Signature = new Uint8Array(Array(64).fill(3)) as SignatureBytes; const programAddress = 'HZMKVnRrWLyQLwPLTTLKtY7ET4Cf7pQugrTr9eTBrpsf' as Address; @@ -256,16 +256,16 @@ describe('decompileTransaction', () => { }); it('converts a partially signed transaction with multiple signers', () => { - const feePayerSignature = new Uint8Array(Array(64).fill(1)) as Ed25519Signature; + const feePayerSignature = new Uint8Array(Array(64).fill(1)) as SignatureBytes; const otherSigner1Address = '3LeBzRE9Yna5zi9R8vdT3MiNQYuEp4gJgVyhhwmqfCtd' as Address; const otherSigner2Address = '8kud9bpNvfemXYdTFjs5cZ8fZinBkx8JAnhVmRwJZk5e' as Address; - const otherSigner2Signature = new Uint8Array(Array(64).fill(3)) as Ed25519Signature; + const otherSigner2Signature = new Uint8Array(Array(64).fill(3)) as SignatureBytes; const programAddress = 'HZMKVnRrWLyQLwPLTTLKtY7ET4Cf7pQugrTr9eTBrpsf' as Address; // Used in the signatures array for a missing signature - const noSignature = new Uint8Array(Array(64).fill(0)) as Ed25519Signature; + const noSignature = new Uint8Array(Array(64).fill(0)) as SignatureBytes; const compiledTransaction: CompiledTransaction = { compiledMessage: { @@ -538,7 +538,7 @@ describe('decompileTransaction', () => { }); it('converts a durable nonce transaction with a single signer', () => { - const feePayerSignature = new Uint8Array(Array(64).fill(1)) as Ed25519Signature; + const feePayerSignature = new Uint8Array(Array(64).fill(1)) as SignatureBytes; const compiledTransaction: CompiledTransaction = { compiledMessage: { @@ -582,8 +582,8 @@ describe('decompileTransaction', () => { }); it('converts a durable nonce transaction with multiple signers', () => { - const feePayerSignature = new Uint8Array(Array(64).fill(1)) as Ed25519Signature; - const authoritySignature = new Uint8Array(Array(64).fill(2)) as Ed25519Signature; + const feePayerSignature = new Uint8Array(Array(64).fill(1)) as SignatureBytes; + const authoritySignature = new Uint8Array(Array(64).fill(2)) as SignatureBytes; const compiledTransaction: CompiledTransaction = { compiledMessage: { @@ -631,11 +631,11 @@ describe('decompileTransaction', () => { it('converts a partially signed durable nonce transaction with multiple signers', () => { const extraSignerAddress = '9bXC3RtDN5MzDMWRCqjgVTeQK2anMhdkq1ZoGN1Tb1UE' as Address; - const feePayerSignature = new Uint8Array(Array(64).fill(1)) as Ed25519Signature; - const extraSignerSignature = new Uint8Array(Array(64).fill(2)) as Ed25519Signature; + const feePayerSignature = new Uint8Array(Array(64).fill(1)) as SignatureBytes; + const extraSignerSignature = new Uint8Array(Array(64).fill(2)) as SignatureBytes; // Used in the signatures array for a missing signature - const noSignature = new Uint8Array(Array(64).fill(0)) as Ed25519Signature; + const noSignature = new Uint8Array(Array(64).fill(0)) as SignatureBytes; const compiledTransaction: CompiledTransaction = { compiledMessage: { diff --git a/packages/transactions/src/__tests__/signatures-test.ts b/packages/transactions/src/__tests__/signatures-test.ts index 1787c2a13035..76880ba91fd2 100644 --- a/packages/transactions/src/__tests__/signatures-test.ts +++ b/packages/transactions/src/__tests__/signatures-test.ts @@ -7,10 +7,8 @@ import { getAddressEncoder, getAddressFromPublicKey, } from '@solana/addresses'; -import { Encoder } from '@solana/codecs-core'; -import { getBase58Encoder } from '@solana/codecs-strings'; import { AccountRole } from '@solana/instructions'; -import { Ed25519Signature, signBytes } from '@solana/keys'; +import { SignatureBytes, signBytes } from '@solana/keys'; import { Blockhash } from '../blockhash'; import { CompilableTransaction } from '../compilable-transaction'; @@ -25,131 +23,13 @@ import { jest.mock('@solana/addresses'); jest.mock('@solana/keys'); jest.mock('../message'); -jest.mock('@solana/codecs-strings', () => ({ - ...jest.requireActual('@solana/codecs-strings'), - getBase58Encoder: jest.fn(), -})); - -// real implementations -const originalBase58Module = jest.requireActual('@solana/codecs-strings'); -const originalGetBase58Encoder = originalBase58Module.getBase58Encoder(); - -describe('assertIsTransactionSignature()', () => { - let assertIsTransactionSignature: typeof import('../signatures').assertIsTransactionSignature; - // Reload `assertIsTransactionSignature` before each test to reset memoized state - beforeEach(async () => { - await jest.isolateModulesAsync(async () => { - const base58ModulePromise = - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - import('../signatures'); - assertIsTransactionSignature = (await base58ModulePromise).assertIsTransactionSignature; - }); - }); - - describe('using the real base58 implementation', () => { - beforeEach(() => { - // use real implementation - jest.mocked(getBase58Encoder).mockReturnValue(originalGetBase58Encoder); - }); - - it('throws when supplied a non-base58 string', () => { - expect(() => { - assertIsTransactionSignature('not-a-base-58-encoded-string'); - }).toThrow(); - }); - it('throws when the decoded byte array has a length other than 32 bytes', () => { - expect(() => { - assertIsTransactionSignature( - // 63 bytes [128, ..., 128] - '1'.repeat(63) - ); - }).toThrow(); - }); - it('does not throw when supplied a base-58 encoded signature', () => { - expect(() => { - // 64 bytes [0, ..., 0] - assertIsTransactionSignature('1'.repeat(64)); - - // example signatures - assertIsTransactionSignature( - '5HkW5GttYoahVHaujuxEyfyq7RwvoKpc94ko5Fq9GuYdyhejg9cHcqm1MjEvHsjaADRe6hVBqB2E4RQgGgxeA2su' - ); - assertIsTransactionSignature( - '2VZm7DkqSKaHxsGiAuVuSkvEbGWf7JrfRdPTw42WKuJC8qw7yQbGL5AE7UxHH3tprgmT9EVbambnK9h3PLpvMvES' - ); - assertIsTransactionSignature( - '5sXRtm61WrRGRTjJ6f2anKUWt86Y4V9gWU4WUpue4T4Zh6zuvFoSyaX5LkEtChfqVC8oHdqLo2eUXbhVduThBdfG' - ); - assertIsTransactionSignature( - '2Dy6Qai5JyChoP4BKoh9KAYhpD96CUhmEce1GJ8HpV5h8Q4CgUt8KZQzhVNDEQYcjARxYyBNhNjhKUGC2XLZtCCm' - ); - }).not.toThrow(); - }); - it('returns undefined when supplied a base-58 encoded signature', () => { - // 64 bytes [0, ..., 0] - expect(assertIsTransactionSignature('1'.repeat(64))).toBeUndefined(); - }); - }); - - describe('using a mock base58 implementation', () => { - const mockEncode = jest.fn(); - beforeEach(() => { - // use mock implementation - mockEncode.mockClear(); - jest.mocked(getBase58Encoder).mockReturnValue({ encode: mockEncode } as unknown as Encoder); - }); - [64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88].forEach( - len => { - it(`attempts to decode input strings of exactly ${len} characters`, () => { - try { - assertIsTransactionSignature('1'.repeat(len)); - // eslint-disable-next-line no-empty - } catch {} - expect(mockEncode).toHaveBeenCalledTimes(1); - }); - } - ); - it('does not attempt to decode too-short input strings', () => { - try { - assertIsTransactionSignature( - // 63 bytes [0, ..., 0] - '1'.repeat(63) - ); - // eslint-disable-next-line no-empty - } catch {} - expect(mockEncode).not.toHaveBeenCalled(); - }); - it('does not attempt to decode too-long input strings', () => { - try { - assertIsTransactionSignature( - // 65 bytes [0, 255, ..., 255] - '167rpwLCuS5DGA8KGZXKsVQ7dnPb9goRLoKfgGbLfQg9WoLUgNY77E2jT11fem3coV9nAkguBACzrU1iyZM4B8roQ' - ); - // eslint-disable-next-line no-empty - } catch {} - expect(mockEncode).not.toHaveBeenCalled(); - }); - it('memoizes getBase58Encoder when called multiple times', () => { - try { - assertIsTransactionSignature('1'.repeat(64)); - // eslint-disable-next-line no-empty - } catch {} - try { - assertIsTransactionSignature('1'.repeat(64)); - // eslint-disable-next-line no-empty - } catch {} - expect(jest.mocked(getBase58Encoder)).toHaveBeenCalledTimes(1); - }); - }); -}); describe('getSignatureFromTransaction', () => { it("returns the signature associated with a transaction's fee payer", () => { const transactionWithoutFeePayerSignature = { feePayer: '123' as Address, signatures: { - ['123' as Address]: new Uint8Array(new Array(64).fill(9)) as Ed25519Signature, + ['123' as Address]: new Uint8Array(new Array(64).fill(9)) as SignatureBytes, } as const, }; expect(getSignatureFromTransaction(transactionWithoutFeePayerSignature)).toBe( @@ -161,7 +41,7 @@ describe('getSignatureFromTransaction', () => { feePayer: '123' as Address, signatures: { // No signature by the fee payer. - ['456' as Address]: new Uint8Array(new Array(64).fill(9)) as Ed25519Signature, + ['456' as Address]: new Uint8Array(new Array(64).fill(9)) as SignatureBytes, } as const, }; expect(() => { @@ -330,11 +210,11 @@ describe('assertTransactionIsFullySigned', () => { const mockProgramAddress = 'program' as Address; const mockPublicKeyAddressA = 'A' as Address; - const mockSignatureA = new Uint8Array(0) as Ed25519Signature; + const mockSignatureA = new Uint8Array(0) as SignatureBytes; const mockPublicKeyAddressB = 'B' as Address; - const mockSignatureB = new Uint8Array(1) as Ed25519Signature; + const mockSignatureB = new Uint8Array(1) as SignatureBytes; const mockPublicKeyAddressC = 'C' as Address; - const mockSignatureC = new Uint8Array(2) as Ed25519Signature; + const mockSignatureC = new Uint8Array(2) as SignatureBytes; const mockBlockhashConstraint = { blockhash: 'a' as Blockhash, diff --git a/packages/transactions/src/__tests__/wire-transaction-test.ts b/packages/transactions/src/__tests__/wire-transaction-test.ts index 2ba32c38669f..d48fc0ccf891 100644 --- a/packages/transactions/src/__tests__/wire-transaction-test.ts +++ b/packages/transactions/src/__tests__/wire-transaction-test.ts @@ -1,6 +1,6 @@ import { Address } from '@solana/addresses'; import { AccountRole } from '@solana/instructions'; -import { Ed25519Signature } from '@solana/keys'; +import { SignatureBytes } from '@solana/keys'; import { Blockhash } from '../blockhash'; import { getBase64EncodedWireTransaction } from '../wire-transaction'; @@ -33,7 +33,7 @@ describe('getBase64EncodedWireTransaction', () => { 0x7f, 0xbc, 0x40, 0xb2, 0x37, 0x06, 0x76, 0xe0, 0xed, 0xa6, 0xef, 0x73, 0x7d, 0x39, 0xfc, 0x30, 0x6c, 0x80, 0x80, 0xc0, 0x66, 0x2d, 0x32, 0x7a, 0x56, 0xb5, 0xb9, 0xd3, 0xc1, 0x20, 0xd7, 0x15, 0xa4, 0x34, 0x3f, 0x93, 0x8a, 0x23, 0xee, 0x08, 0xfb, 0x82, 0x3e, 0xe0, 0x8f, 0xb8, 0x23, 0xee, - ]) as Ed25519Signature, + ]) as SignatureBytes, }, version: 0, }; diff --git a/packages/transactions/src/compile-transaction.ts b/packages/transactions/src/compile-transaction.ts index d89c2f4fbe23..64ffc3634a8f 100644 --- a/packages/transactions/src/compile-transaction.ts +++ b/packages/transactions/src/compile-transaction.ts @@ -1,4 +1,4 @@ -import { Ed25519Signature } from '@solana/keys'; +import { SignatureBytes } from '@solana/keys'; import { CompilableTransaction } from './compilable-transaction'; import { CompiledMessage, compileMessage } from './message'; @@ -6,7 +6,7 @@ import { ITransactionWithSignatures } from './signatures'; export type CompiledTransaction = Readonly<{ compiledMessage: CompiledMessage; - signatures: Ed25519Signature[]; + signatures: SignatureBytes[]; }>; export function getCompiledTransaction( diff --git a/packages/transactions/src/decompile-transaction.ts b/packages/transactions/src/decompile-transaction.ts index 7c8bda7d42b6..52300a9fbd5d 100644 --- a/packages/transactions/src/decompile-transaction.ts +++ b/packages/transactions/src/decompile-transaction.ts @@ -1,7 +1,7 @@ import { Address, assertIsAddress } from '@solana/addresses'; import { pipe } from '@solana/functional'; import { AccountRole, IAccountMeta, IInstruction } from '@solana/instructions'; -import { Ed25519Signature } from '@solana/keys'; +import { SignatureBytes } from '@solana/keys'; import { Blockhash, setTransactionLifetimeUsingBlockhash } from './blockhash'; import { CompilableTransaction } from './compilable-transaction'; @@ -128,7 +128,7 @@ function convertSignatures(compiledTransaction: CompiledTransaction): ITransacti if (allZeros) return acc; const address = staticAccounts[index]; - return { ...acc, [address]: sig as Ed25519Signature }; + return { ...acc, [address]: sig as SignatureBytes }; }, {}); } diff --git a/packages/transactions/src/serializers/transaction.ts b/packages/transactions/src/serializers/transaction.ts index e6bf281076de..4a909edd77c2 100644 --- a/packages/transactions/src/serializers/transaction.ts +++ b/packages/transactions/src/serializers/transaction.ts @@ -8,7 +8,7 @@ import { getStructEncoder, } from '@solana/codecs-data-structures'; import { getShortU16Decoder, getShortU16Encoder } from '@solana/codecs-numbers'; -import { Ed25519Signature } from '@solana/keys'; +import { SignatureBytes } from '@solana/keys'; import { CompilableTransaction } from '../compilable-transaction'; import { CompiledTransaction, getCompiledTransaction } from '../compile-transaction'; @@ -37,8 +37,8 @@ function getCompiledTransactionEncoder(): Encoder { ); } -function getSignatureDecoder(): Decoder { - return mapDecoder(getBytesDecoder({ size: 64 }), bytes => bytes as Ed25519Signature); +function getSignatureDecoder(): Decoder { + return mapDecoder(getBytesDecoder({ size: 64 }), bytes => bytes as SignatureBytes); } function getCompiledTransactionDecoder(): Decoder { diff --git a/packages/transactions/src/signatures.ts b/packages/transactions/src/signatures.ts index 26ec743b007d..61ae74f5810d 100644 --- a/packages/transactions/src/signatures.ts +++ b/packages/transactions/src/signatures.ts @@ -1,8 +1,8 @@ import { Address, getAddressFromPublicKey } from '@solana/addresses'; -import { Decoder, Encoder } from '@solana/codecs-core'; -import { getBase58Decoder, getBase58Encoder } from '@solana/codecs-strings'; +import { Decoder } from '@solana/codecs-core'; +import { getBase58Decoder } from '@solana/codecs-strings'; import { isSignerRole } from '@solana/instructions'; -import { Ed25519Signature, signBytes } from '@solana/keys'; +import { Signature, SignatureBytes, signBytes } from '@solana/keys'; import { CompilableTransaction } from './compilable-transaction'; import { ITransactionWithFeePayer } from './fee-payer'; @@ -13,68 +13,14 @@ export interface IFullySignedTransaction extends ITransactionWithSignatures { readonly __brand: unique symbol; } export interface ITransactionWithSignatures { - readonly signatures: Readonly>; + readonly signatures: Readonly>; } -export type TransactionSignature = string & { readonly __brand: unique symbol }; - -let base58Encoder: Encoder | undefined; let base58Decoder: Decoder | undefined; -export function assertIsTransactionSignature( - putativeTransactionSignature: string -): asserts putativeTransactionSignature is TransactionSignature { - if (!base58Encoder) base58Encoder = getBase58Encoder(); - - try { - // Fast-path; see if the input string is of an acceptable length. - if ( - // Lowest value (64 bytes of zeroes) - putativeTransactionSignature.length < 64 || - // Highest value (64 bytes of 255) - putativeTransactionSignature.length > 88 - ) { - throw new Error('Expected input string to decode to a byte array of length 64.'); - } - // Slow-path; actually attempt to decode the input string. - const bytes = base58Encoder.encode(putativeTransactionSignature); - const numBytes = bytes.byteLength; - if (numBytes !== 64) { - throw new Error(`Expected input string to decode to a byte array of length 64. Actual length: ${numBytes}`); - } - } catch (e) { - throw new Error(`\`${putativeTransactionSignature}\` is not a transaction signature`, { - cause: e, - }); - } -} - -export function isTransactionSignature( - putativeTransactionSignature: string -): putativeTransactionSignature is TransactionSignature { - if (!base58Encoder) base58Encoder = getBase58Encoder(); - - // Fast-path; see if the input string is of an acceptable length. - if ( - // Lowest value (64 bytes of zeroes) - putativeTransactionSignature.length < 64 || - // Highest value (64 bytes of 255) - putativeTransactionSignature.length > 88 - ) { - return false; - } - // Slow-path; actually attempt to decode the input string. - const bytes = base58Encoder.encode(putativeTransactionSignature); - const numBytes = bytes.byteLength; - if (numBytes !== 64) { - return false; - } - return true; -} - export function getSignatureFromTransaction( transaction: ITransactionWithFeePayer & ITransactionWithSignatures -): TransactionSignature { +): Signature { if (!base58Decoder) base58Decoder = getBase58Decoder(); const signatureBytes = transaction.signatures[transaction.feePayer]; @@ -86,7 +32,7 @@ export function getSignatureFromTransaction( ); } const transactionSignature = base58Decoder.decode(signatureBytes)[0]; - return transactionSignature as TransactionSignature; + return transactionSignature as Signature; } export async function signTransaction( @@ -94,7 +40,7 @@ export async function signTransaction { const compiledMessage = compileMessage(transaction); - const nextSignatures: Record = + const nextSignatures: Record = 'signatures' in transaction ? { ...transaction.signatures } : {}; const wireMessageBytes = getCompiledMessageEncoder().encode(compiledMessage); const publicKeySignaturePairs = await Promise.all( @@ -113,11 +59,6 @@ export async function signTransaction( transaction: TTransaction & ITransactionWithSignatures ): asserts transaction is TTransaction & IFullySignedTransaction { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2956684d0215..b7d53465b72c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -723,6 +723,12 @@ importers: '@solana/assertions': specifier: workspace:* version: link:../assertions + '@solana/codecs-core': + specifier: workspace:* + version: link:../codecs-core + '@solana/codecs-strings': + specifier: workspace:* + version: link:../codecs-strings devDependencies: '@solana/eslint-config-solana': specifier: ^1.0.2 @@ -1183,6 +1189,9 @@ importers: '@solana/eslint-config-solana': specifier: ^1.0.2 version: 1.0.2(@typescript-eslint/eslint-plugin@6.7.0)(@typescript-eslint/parser@6.3.0)(eslint-plugin-jest@27.4.2)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-simple-import-sort@10.0.0)(eslint-plugin-sort-keys-fix@1.1.2)(eslint@8.45.0)(typescript@5.2.2) + '@solana/keys': + specifier: workspace:* + version: link:../keys '@solana/rpc-transport': specifier: workspace:* version: link:../rpc-transport @@ -1265,6 +1274,9 @@ importers: '@solana/eslint-config-solana': specifier: ^1.0.2 version: 1.0.2(@typescript-eslint/eslint-plugin@6.7.0)(@typescript-eslint/parser@6.3.0)(eslint-plugin-jest@27.4.2)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-simple-import-sort@10.0.0)(eslint-plugin-sort-keys-fix@1.1.2)(eslint@8.45.0)(typescript@5.2.2) + '@solana/keys': + specifier: workspace:* + version: link:../keys '@solana/rpc-core': specifier: workspace:* version: link:../rpc-core