7
7
state:: Mint ,
8
8
} ,
9
9
solana_program:: { instruction:: Instruction , program_error:: ProgramError , pubkey:: Pubkey } ,
10
+ spl_transfer_hook_interface:: offchain:: add_extra_account_metas_for_execute,
10
11
std:: future:: Future ,
11
12
} ;
12
13
32
33
/// &mint,
33
34
/// ).await?;
34
35
/// ```
36
+ #[ deprecated(
37
+ since = "1.1.0" ,
38
+ note = "Please use `create_transfer_checked_instruction_with_extra_metas` instead"
39
+ ) ]
35
40
pub async fn resolve_extra_transfer_account_metas < F , Fut > (
36
41
instruction : & mut Instruction ,
37
42
fetch_account_data_fn : F ,
@@ -57,3 +62,254 @@ where
57
62
}
58
63
Ok ( ( ) )
59
64
}
65
+
66
+ /// Offchain helper to create a `TransferChecked` instruction with all
67
+ /// additional required account metas for a transfer, including the ones
68
+ /// required by the transfer hook.
69
+ ///
70
+ /// To be client-agnostic and to avoid pulling in the full solana-sdk, this
71
+ /// simply takes a function that will return its data as `Future<Vec<u8>>` for
72
+ /// the given address. Can be called in the following way:
73
+ ///
74
+ /// ```rust,ignore
75
+ /// let instruction = create_transfer_checked_instruction_with_extra_metas(
76
+ /// &spl_token_2022::id(),
77
+ /// &source,
78
+ /// &mint,
79
+ /// &destination,
80
+ /// &authority,
81
+ /// &[],
82
+ /// amount,
83
+ /// decimals,
84
+ /// |address| self.client.get_account(&address).map_ok(|opt| opt.map(|acc| acc.data)),
85
+ /// )
86
+ /// .await?
87
+ /// ```
88
+ #[ allow( clippy:: too_many_arguments) ]
89
+ pub async fn create_transfer_checked_instruction_with_extra_metas < F , Fut > (
90
+ token_program_id : & Pubkey ,
91
+ source_pubkey : & Pubkey ,
92
+ mint_pubkey : & Pubkey ,
93
+ destination_pubkey : & Pubkey ,
94
+ authority_pubkey : & Pubkey ,
95
+ signer_pubkeys : & [ & Pubkey ] ,
96
+ amount : u64 ,
97
+ decimals : u8 ,
98
+ fetch_account_data_fn : F ,
99
+ ) -> Result < Instruction , AccountFetchError >
100
+ where
101
+ F : Fn ( Pubkey ) -> Fut ,
102
+ Fut : Future < Output = AccountDataResult > ,
103
+ {
104
+ let mut transfer_instruction = crate :: instruction:: transfer_checked (
105
+ token_program_id,
106
+ source_pubkey,
107
+ mint_pubkey,
108
+ destination_pubkey,
109
+ authority_pubkey,
110
+ signer_pubkeys,
111
+ amount,
112
+ decimals,
113
+ ) ?;
114
+
115
+ let mint_data = fetch_account_data_fn ( * mint_pubkey)
116
+ . await ?
117
+ . ok_or ( ProgramError :: InvalidAccountData ) ?;
118
+ let mint = StateWithExtensions :: < Mint > :: unpack ( & mint_data) ?;
119
+
120
+ if let Some ( program_id) = transfer_hook:: get_program_id ( & mint) {
121
+ add_extra_account_metas_for_execute (
122
+ & mut transfer_instruction,
123
+ & program_id,
124
+ source_pubkey,
125
+ mint_pubkey,
126
+ destination_pubkey,
127
+ authority_pubkey,
128
+ amount,
129
+ fetch_account_data_fn,
130
+ )
131
+ . await ?;
132
+ }
133
+
134
+ Ok ( transfer_instruction)
135
+ }
136
+
137
+ #[ cfg( test) ]
138
+ mod tests {
139
+ use {
140
+ super :: * ,
141
+ crate :: extension:: { transfer_hook:: TransferHook , ExtensionType , StateWithExtensionsMut } ,
142
+ solana_program:: { instruction:: AccountMeta , program_option:: COption } ,
143
+ solana_program_test:: tokio,
144
+ spl_pod:: optional_keys:: OptionalNonZeroPubkey ,
145
+ spl_tlv_account_resolution:: {
146
+ account:: ExtraAccountMeta , seeds:: Seed , state:: ExtraAccountMetaList ,
147
+ } ,
148
+ spl_transfer_hook_interface:: {
149
+ get_extra_account_metas_address, instruction:: ExecuteInstruction ,
150
+ } ,
151
+ } ;
152
+
153
+ const DECIMALS : u8 = 0 ;
154
+ const MINT_PUBKEY : Pubkey = Pubkey :: new_from_array ( [ 1u8 ; 32 ] ) ;
155
+ const TRANSFER_HOOK_PROGRAM_ID : Pubkey = Pubkey :: new_from_array ( [ 2u8 ; 32 ] ) ;
156
+ const EXTRA_META_1 : Pubkey = Pubkey :: new_from_array ( [ 3u8 ; 32 ] ) ;
157
+ const EXTRA_META_2 : Pubkey = Pubkey :: new_from_array ( [ 4u8 ; 32 ] ) ;
158
+
159
+ // Mock to return the mint data or the validation state account data
160
+ async fn mock_fetch_account_data_fn ( address : Pubkey ) -> AccountDataResult {
161
+ if address == MINT_PUBKEY {
162
+ let mint_len =
163
+ ExtensionType :: try_calculate_account_len :: < Mint > ( & [ ExtensionType :: TransferHook ] )
164
+ . unwrap ( ) ;
165
+ let mut data = vec ! [ 0u8 ; mint_len] ;
166
+ let mut mint = StateWithExtensionsMut :: < Mint > :: unpack_uninitialized ( & mut data) . unwrap ( ) ;
167
+
168
+ let extension = mint. init_extension :: < TransferHook > ( true ) . unwrap ( ) ;
169
+ extension. program_id =
170
+ OptionalNonZeroPubkey :: try_from ( Some ( TRANSFER_HOOK_PROGRAM_ID ) ) . unwrap ( ) ;
171
+
172
+ mint. base . mint_authority = COption :: Some ( Pubkey :: new_unique ( ) ) ;
173
+ mint. base . decimals = DECIMALS ;
174
+ mint. base . is_initialized = true ;
175
+ mint. base . freeze_authority = COption :: None ;
176
+ mint. pack_base ( ) ;
177
+ mint. init_account_type ( ) . unwrap ( ) ;
178
+
179
+ Ok ( Some ( data) )
180
+ } else if address
181
+ == get_extra_account_metas_address ( & MINT_PUBKEY , & TRANSFER_HOOK_PROGRAM_ID )
182
+ {
183
+ let extra_metas = vec ! [
184
+ ExtraAccountMeta :: new_with_pubkey( & EXTRA_META_1 , true , false ) . unwrap( ) ,
185
+ ExtraAccountMeta :: new_with_pubkey( & EXTRA_META_2 , true , false ) . unwrap( ) ,
186
+ ExtraAccountMeta :: new_with_seeds(
187
+ & [
188
+ Seed :: AccountKey { index: 0 } , // source
189
+ Seed :: AccountKey { index: 2 } , // destination
190
+ Seed :: AccountKey { index: 4 } , // validation state
191
+ ] ,
192
+ false ,
193
+ true ,
194
+ )
195
+ . unwrap( ) ,
196
+ ExtraAccountMeta :: new_with_seeds(
197
+ & [
198
+ Seed :: InstructionData {
199
+ index: 8 ,
200
+ length: 8 ,
201
+ } , // amount
202
+ Seed :: AccountKey { index: 2 } , // destination
203
+ Seed :: AccountKey { index: 5 } , // extra meta 1
204
+ Seed :: AccountKey { index: 7 } , // extra meta 3 (PDA)
205
+ ] ,
206
+ false ,
207
+ true ,
208
+ )
209
+ . unwrap( ) ,
210
+ ] ;
211
+ let account_size = ExtraAccountMetaList :: size_of ( extra_metas. len ( ) ) . unwrap ( ) ;
212
+ let mut data = vec ! [ 0u8 ; account_size] ;
213
+ ExtraAccountMetaList :: init :: < ExecuteInstruction > ( & mut data, & extra_metas) ?;
214
+ Ok ( Some ( data) )
215
+ } else {
216
+ Ok ( None )
217
+ }
218
+ }
219
+
220
+ #[ tokio:: test]
221
+ async fn test_create_transfer_checked_instruction_with_extra_metas ( ) {
222
+ let source = Pubkey :: new_unique ( ) ;
223
+ let destination = Pubkey :: new_unique ( ) ;
224
+ let authority = Pubkey :: new_unique ( ) ;
225
+ let amount = 100u64 ;
226
+
227
+ let validate_state_pubkey =
228
+ get_extra_account_metas_address ( & MINT_PUBKEY , & TRANSFER_HOOK_PROGRAM_ID ) ;
229
+ let extra_meta_3_pubkey = Pubkey :: find_program_address (
230
+ & [
231
+ source. as_ref ( ) ,
232
+ destination. as_ref ( ) ,
233
+ validate_state_pubkey. as_ref ( ) ,
234
+ ] ,
235
+ & TRANSFER_HOOK_PROGRAM_ID ,
236
+ )
237
+ . 0 ;
238
+ let extra_meta_4_pubkey = Pubkey :: find_program_address (
239
+ & [
240
+ amount. to_le_bytes ( ) . as_ref ( ) ,
241
+ destination. as_ref ( ) ,
242
+ EXTRA_META_1 . as_ref ( ) ,
243
+ extra_meta_3_pubkey. as_ref ( ) ,
244
+ ] ,
245
+ & TRANSFER_HOOK_PROGRAM_ID ,
246
+ )
247
+ . 0 ;
248
+
249
+ let instruction = create_transfer_checked_instruction_with_extra_metas (
250
+ & crate :: id ( ) ,
251
+ & source,
252
+ & MINT_PUBKEY ,
253
+ & destination,
254
+ & authority,
255
+ & [ ] ,
256
+ amount,
257
+ DECIMALS ,
258
+ mock_fetch_account_data_fn,
259
+ )
260
+ . await
261
+ . unwrap ( ) ;
262
+
263
+ let check_metas = [
264
+ AccountMeta :: new ( source, false ) ,
265
+ AccountMeta :: new_readonly ( MINT_PUBKEY , false ) ,
266
+ AccountMeta :: new ( destination, false ) ,
267
+ AccountMeta :: new_readonly ( authority, true ) ,
268
+ AccountMeta :: new_readonly ( EXTRA_META_1 , true ) ,
269
+ AccountMeta :: new_readonly ( EXTRA_META_2 , true ) ,
270
+ AccountMeta :: new ( extra_meta_3_pubkey, false ) ,
271
+ AccountMeta :: new ( extra_meta_4_pubkey, false ) ,
272
+ AccountMeta :: new_readonly ( TRANSFER_HOOK_PROGRAM_ID , false ) ,
273
+ AccountMeta :: new_readonly ( validate_state_pubkey, false ) ,
274
+ ] ;
275
+
276
+ assert_eq ! ( instruction. accounts, check_metas) ;
277
+
278
+ // With additional signers
279
+ let signer_1 = Pubkey :: new_unique ( ) ;
280
+ let signer_2 = Pubkey :: new_unique ( ) ;
281
+ let signer_3 = Pubkey :: new_unique ( ) ;
282
+
283
+ let instruction = create_transfer_checked_instruction_with_extra_metas (
284
+ & crate :: id ( ) ,
285
+ & source,
286
+ & MINT_PUBKEY ,
287
+ & destination,
288
+ & authority,
289
+ & [ & signer_1, & signer_2, & signer_3] ,
290
+ amount,
291
+ DECIMALS ,
292
+ mock_fetch_account_data_fn,
293
+ )
294
+ . await
295
+ . unwrap ( ) ;
296
+
297
+ let check_metas = [
298
+ AccountMeta :: new ( source, false ) ,
299
+ AccountMeta :: new_readonly ( MINT_PUBKEY , false ) ,
300
+ AccountMeta :: new ( destination, false ) ,
301
+ AccountMeta :: new_readonly ( authority, false ) , // False because of additional signers
302
+ AccountMeta :: new_readonly ( signer_1, true ) ,
303
+ AccountMeta :: new_readonly ( signer_2, true ) ,
304
+ AccountMeta :: new_readonly ( signer_3, true ) ,
305
+ AccountMeta :: new_readonly ( EXTRA_META_1 , true ) ,
306
+ AccountMeta :: new_readonly ( EXTRA_META_2 , true ) ,
307
+ AccountMeta :: new ( extra_meta_3_pubkey, false ) ,
308
+ AccountMeta :: new ( extra_meta_4_pubkey, false ) ,
309
+ AccountMeta :: new_readonly ( TRANSFER_HOOK_PROGRAM_ID , false ) ,
310
+ AccountMeta :: new_readonly ( validate_state_pubkey, false ) ,
311
+ ] ;
312
+
313
+ assert_eq ! ( instruction. accounts, check_metas) ;
314
+ }
315
+ }
0 commit comments