11import { Address } from '@solana/addresses' ;
2- import type { VariableSizeCodec } from '@solana/codecs-core' ;
3- import { SOLANA_ERROR__SIGNER__WALLET_MULTISIGN_UNIMPLEMENTED , SolanaError } from '@solana/errors' ;
2+ import type { VariableSizeCodec , VariableSizeDecoder } from '@solana/codecs-core' ;
3+ import {
4+ SOLANA_ERROR__SIGNER__WALLET_MULTISIGN_UNIMPLEMENTED ,
5+ SOLANA_ERROR__TRANSACTION__NONCE_ACCOUNT_CANNOT_BE_IN_LOOKUP_TABLE ,
6+ SolanaError ,
7+ } from '@solana/errors' ;
48import { SignatureBytes } from '@solana/keys' ;
5- import { Transaction , TransactionMessageBytes } from '@solana/transactions' ;
9+ import { Blockhash } from '@solana/rpc-types' ;
10+ import {
11+ CompiledTransactionMessage ,
12+ CompiledTransactionMessageWithLifetime ,
13+ getCompiledTransactionMessageDecoder ,
14+ } from '@solana/transaction-messages' ;
15+ import {
16+ getTransactionLifetimeConstraintFromCompiledTransactionMessage ,
17+ Transaction ,
18+ TransactionMessageBytes ,
19+ } from '@solana/transactions' ;
620import { getTransactionCodec } from '@solana/transactions' ;
721import type { UiWalletAccount } from '@wallet-standard/ui' ;
822
923import { renderHook } from '../test-renderer' ;
1024import { useSignTransaction } from '../useSignTransaction' ;
1125import { useWalletAccountTransactionSigner } from '../useWalletAccountTransactionSigner' ;
1226
27+ jest . mock ( '@solana/transaction-messages' ) ;
1328jest . mock ( '@solana/transactions' ) ;
1429jest . mock ( '../useSignTransaction' ) ;
1530
@@ -120,8 +135,8 @@ describe('useWalletAccountTransactionSigner', () => {
120135 } ) ;
121136 it ( 'decodes and returns the signed transaction bytes returned by `signTransactions`' , async ( ) => {
122137 expect . assertions ( 2 ) ;
123- const mockSignedTransaction = new Uint8Array ( [ 1 , 2 , 3 ] ) ;
124- const mockDecodedTransaction = { } as Transaction ;
138+ const mockSignedTransaction = new Uint8Array ( [ 1 , 2 , 3 , 4 , 5 , 6 ] ) ;
139+ const mockDecodedTransaction = { messageBytes : [ 1 , 2 , 3 ] } as unknown as Transaction ;
125140 mockSignTransaction . mockResolvedValue ( { signedTransaction : mockSignedTransaction } ) ;
126141 mockDecodeTransaction . mockReturnValue ( mockDecodedTransaction ) ;
127142 const { result } = renderHook ( ( ) => useWalletAccountTransactionSigner ( mockUiWalletAccount , 'solana:danknet' ) ) ;
@@ -130,7 +145,9 @@ describe('useWalletAccountTransactionSigner', () => {
130145 throw result . current ;
131146 } else {
132147 const { modifyAndSignTransactions } = result . current ;
148+ const lifetimeConstraint = { blockhash : 'abc' , lastValidBlockHeight : 123n } ;
133149 const inputTransaction = {
150+ lifetimeConstraint,
134151 messageBytes : new Uint8Array ( [ 1 , 2 , 3 ] ) as unknown as TransactionMessageBytes ,
135152 signatures : {
136153 '11111111111111111111111111111114' : new Uint8Array ( 64 ) . fill ( 2 ) as SignatureBytes ,
@@ -141,7 +158,7 @@ describe('useWalletAccountTransactionSigner', () => {
141158 // eslint-disable-next-line jest/no-conditional-expect
142159 expect ( mockDecodeTransaction ) . toHaveBeenCalledWith ( mockSignedTransaction ) ;
143160 // eslint-disable-next-line jest/no-conditional-expect
144- await expect ( signPromise ) . resolves . toEqual ( [ mockDecodedTransaction ] ) ;
161+ await expect ( signPromise ) . resolves . toEqual ( [ { ... mockDecodedTransaction , lifetimeConstraint } ] ) ;
145162 }
146163 } ) ;
147164 it ( 'calls `signTransaction` with all options except the `abortSignal`' , ( ) => {
@@ -193,4 +210,133 @@ describe('useWalletAccountTransactionSigner', () => {
193210 await expect ( signPromise ) . rejects . toThrow ( new Error ( 'o no' ) ) ;
194211 }
195212 } ) ;
213+ it ( 'returns an unchanged lifetime constraint if the signed transaction has the same one' , async ( ) => {
214+ expect . assertions ( 1 ) ;
215+ const mockSignedTransaction = new Uint8Array ( [ 1 , 2 , 3 , 4 , 5 , 6 ] ) ;
216+ const mockDecodedTransaction = { messageBytes : [ 4 , 5 , 6 ] } as unknown as Transaction ;
217+ mockSignTransaction . mockResolvedValue ( { signedTransaction : mockSignedTransaction } ) ;
218+ mockDecodeTransaction . mockReturnValue ( mockDecodedTransaction ) ;
219+ const { result } = renderHook ( ( ) => useWalletAccountTransactionSigner ( mockUiWalletAccount , 'solana:danknet' ) ) ;
220+ // eslint-disable-next-line jest/no-conditional-in-test
221+ if ( result . __type === 'error' || ! result . current ) {
222+ throw result . current ;
223+ } else {
224+ const { modifyAndSignTransactions } = result . current ;
225+ const lifetimeConstraint = { blockhash : 'abc' , lastValidBlockHeight : 123n } ;
226+ const inputTransaction = {
227+ lifetimeConstraint,
228+ messageBytes : new Uint8Array ( [ 1 , 2 , 3 ] ) as unknown as TransactionMessageBytes ,
229+ signatures : { } ,
230+ } ;
231+ jest . mocked ( getCompiledTransactionMessageDecoder ) . mockReturnValue ( {
232+ decode : jest . fn ( ) . mockReturnValue ( {
233+ lifetimeToken : 'abc' ,
234+ } ) ,
235+ } as unknown as VariableSizeDecoder < CompiledTransactionMessage & CompiledTransactionMessageWithLifetime > ) ;
236+ const signPromise = modifyAndSignTransactions ( [ inputTransaction ] ) ;
237+ await jest . runAllTimersAsync ( ) ;
238+ // eslint-disable-next-line jest/no-conditional-expect
239+ await expect ( signPromise ) . resolves . toEqual ( [ { ...mockDecodedTransaction , lifetimeConstraint } ] ) ;
240+ }
241+ } ) ;
242+ it ( 'returns a new lifetime constraint if the input transaction does not have one' , async ( ) => {
243+ expect . assertions ( 1 ) ;
244+ const mockSignedTransaction = new Uint8Array ( [ 1 , 2 , 3 , 4 , 5 , 6 ] ) ;
245+ const mockDecodedTransaction = { messageBytes : [ 4 , 5 , 6 ] } as unknown as Transaction ;
246+ mockSignTransaction . mockResolvedValue ( { signedTransaction : mockSignedTransaction } ) ;
247+ mockDecodeTransaction . mockReturnValue ( mockDecodedTransaction ) ;
248+ const { result } = renderHook ( ( ) => useWalletAccountTransactionSigner ( mockUiWalletAccount , 'solana:danknet' ) ) ;
249+ // eslint-disable-next-line jest/no-conditional-in-test
250+ if ( result . __type === 'error' || ! result . current ) {
251+ throw result . current ;
252+ } else {
253+ const { modifyAndSignTransactions } = result . current ;
254+ const inputTransaction = {
255+ messageBytes : new Uint8Array ( [ 1 , 2 , 3 ] ) as unknown as TransactionMessageBytes ,
256+ signatures : { } ,
257+ } ;
258+ jest . mocked ( getCompiledTransactionMessageDecoder ) . mockReturnValue ( {
259+ decode : jest . fn ( ) . mockReturnValue ( { } ) ,
260+ } as unknown as VariableSizeDecoder < CompiledTransactionMessage & CompiledTransactionMessageWithLifetime > ) ;
261+ const newLifetimeConstraint = { blockhash : 'abc' as Blockhash , lastValidBlockHeight : 123n } ;
262+ jest . mocked ( getTransactionLifetimeConstraintFromCompiledTransactionMessage ) . mockResolvedValue (
263+ newLifetimeConstraint ,
264+ ) ;
265+ const signPromise = modifyAndSignTransactions ( [ inputTransaction ] ) ;
266+ await jest . runAllTimersAsync ( ) ;
267+ // eslint-disable-next-line jest/no-conditional-expect
268+ await expect ( signPromise ) . resolves . toEqual ( [
269+ { ...mockDecodedTransaction , lifetimeConstraint : newLifetimeConstraint } ,
270+ ] ) ;
271+ }
272+ } ) ;
273+ it ( 'returns a new lifetime constraint if the signed transaction has a different one' , async ( ) => {
274+ expect . assertions ( 1 ) ;
275+ const mockSignedTransaction = new Uint8Array ( [ 1 , 2 , 3 , 4 , 5 , 6 ] ) ;
276+ const mockDecodedTransaction = { messageBytes : [ 4 , 5 , 6 ] } as unknown as Transaction ;
277+ mockSignTransaction . mockResolvedValue ( { signedTransaction : mockSignedTransaction } ) ;
278+ mockDecodeTransaction . mockReturnValue ( mockDecodedTransaction ) ;
279+ const { result } = renderHook ( ( ) => useWalletAccountTransactionSigner ( mockUiWalletAccount , 'solana:danknet' ) ) ;
280+ // eslint-disable-next-line jest/no-conditional-in-test
281+ if ( result . __type === 'error' || ! result . current ) {
282+ throw result . current ;
283+ } else {
284+ const { modifyAndSignTransactions } = result . current ;
285+ const inputLifetimeConstraint = { blockhash : 'abc' , lastValidBlockHeight : 123n } ;
286+ const inputTransaction = {
287+ lifetimeConstraint : inputLifetimeConstraint ,
288+ messageBytes : new Uint8Array ( [ 1 , 2 , 3 ] ) as unknown as TransactionMessageBytes ,
289+ signatures : { } ,
290+ } ;
291+ jest . mocked ( getCompiledTransactionMessageDecoder ) . mockReturnValue ( {
292+ decode : jest . fn ( ) . mockReturnValue ( {
293+ lifetimeToken : 'def' ,
294+ } ) ,
295+ } as unknown as VariableSizeDecoder < CompiledTransactionMessage & CompiledTransactionMessageWithLifetime > ) ;
296+ const newLifetimeConstraint = { blockhash : 'def' as Blockhash , lastValidBlockHeight : 456n } ;
297+ jest . mocked ( getTransactionLifetimeConstraintFromCompiledTransactionMessage ) . mockResolvedValue (
298+ newLifetimeConstraint ,
299+ ) ;
300+ const signPromise = modifyAndSignTransactions ( [ inputTransaction ] ) ;
301+ await jest . runAllTimersAsync ( ) ;
302+ // eslint-disable-next-line jest/no-conditional-expect
303+ await expect ( signPromise ) . resolves . toEqual ( [
304+ { ...mockDecodedTransaction , lifetimeConstraint : newLifetimeConstraint } ,
305+ ] ) ;
306+ }
307+ } ) ;
308+ it ( 'fatals when the signed transaction has a new durable nonce lifetime but the nonce account is only in a lookup table' , async ( ) => {
309+ expect . assertions ( 1 ) ;
310+ const mockSignedTransaction = new Uint8Array ( [ 1 , 2 , 3 , 4 , 5 , 6 ] ) ;
311+ const mockDecodedTransaction = { messageBytes : [ 4 , 5 , 6 ] } as unknown as Transaction ;
312+ mockSignTransaction . mockResolvedValue ( { signedTransaction : mockSignedTransaction } ) ;
313+ mockDecodeTransaction . mockReturnValue ( mockDecodedTransaction ) ;
314+ const { result } = renderHook ( ( ) => useWalletAccountTransactionSigner ( mockUiWalletAccount , 'solana:danknet' ) ) ;
315+ // eslint-disable-next-line jest/no-conditional-in-test
316+ if ( result . __type === 'error' || ! result . current ) {
317+ throw result . current ;
318+ } else {
319+ const { modifyAndSignTransactions } = result . current ;
320+ const inputTransaction = {
321+ messageBytes : new Uint8Array ( [ 1 , 2 , 3 ] ) as unknown as TransactionMessageBytes ,
322+ signatures : { } ,
323+ } ;
324+ jest . mocked ( getCompiledTransactionMessageDecoder ) . mockReturnValue ( {
325+ decode : jest . fn ( ) . mockReturnValue ( { } ) ,
326+ } as unknown as VariableSizeDecoder < CompiledTransactionMessage & CompiledTransactionMessageWithLifetime > ) ;
327+ jest . mocked ( getTransactionLifetimeConstraintFromCompiledTransactionMessage ) . mockRejectedValue (
328+ new SolanaError ( SOLANA_ERROR__TRANSACTION__NONCE_ACCOUNT_CANNOT_BE_IN_LOOKUP_TABLE , {
329+ nonce : 'abc' ,
330+ } ) ,
331+ ) ;
332+ const signPromise = modifyAndSignTransactions ( [ inputTransaction ] ) ;
333+ await jest . runAllTimersAsync ( ) ;
334+ // eslint-disable-next-line jest/no-conditional-expect
335+ await expect ( signPromise ) . rejects . toThrow (
336+ new SolanaError ( SOLANA_ERROR__TRANSACTION__NONCE_ACCOUNT_CANNOT_BE_IN_LOOKUP_TABLE , {
337+ nonce : 'abc' ,
338+ } ) ,
339+ ) ;
340+ }
341+ } ) ;
196342} ) ;
0 commit comments