@@ -31,7 +31,7 @@ describe('recipientUtils', function () {
3131 'defiDeposit' ,
3232 'defiWithdraw' ,
3333 'contractCall' ,
34- // Staking
34+ // Staking — 'delegate' also covers SOL solDelegateIntent
3535 'delegate' ,
3636 'undelegate' ,
3737 'switchValidator' ,
@@ -53,6 +53,8 @@ describe('recipientUtils', function () {
5353 // Avalanche / Flare cross-chain atomic imports
5454 'import' ,
5555 'importtoc' ,
56+ // SOL: deactivate stake account (solDeactivateIntent)
57+ 'deactivate' ,
5658 ] ;
5759 expected . forEach ( ( t ) => assert . ok ( NO_RECIPIENT_TX_TYPES . has ( t ) , `${ t } should be in NO_RECIPIENT_TX_TYPES` ) ) ;
5860 assert . strictEqual ( NO_RECIPIENT_TX_TYPES . size , expected . length ) ;
@@ -77,12 +79,7 @@ describe('recipientUtils', function () {
7779 const txRequest = makeTxRequest ( {
7880 intent : {
7981 intentType : 'payment' ,
80- recipients : [
81- {
82- address : { address : '0xabc' } ,
83- amount : { value : '500' , symbol : 'eth' } ,
84- } ,
85- ] ,
82+ recipients : [ { address : { address : '0xabc' } , amount : { value : '500' , symbol : 'eth' } } ] ,
8683 } as any ,
8784 } ) ;
8885 const result = resolveEffectiveTxParams ( txRequest , { } ) ;
@@ -92,9 +89,7 @@ describe('recipientUtils', function () {
9289 } ) ;
9390
9491 it ( 'resolves txType from intent.intentType when txParams.type is absent' , function ( ) {
95- const txRequest = makeTxRequest ( {
96- intent : { intentType : 'consolidate' } as any ,
97- } ) ;
92+ const txRequest = makeTxRequest ( { intent : { intentType : 'consolidate' } as any } ) ;
9893 const result = resolveEffectiveTxParams ( txRequest , { } ) ;
9994 assert . strictEqual ( result . type , 'consolidate' ) ;
10095 } ) ;
@@ -110,26 +105,21 @@ describe('recipientUtils', function () {
110105 'pledge' ,
111106 'import' ,
112107 'importtoc' ,
108+ 'deactivate' ,
113109 ] ) {
114110 const txRequest = makeTxRequest ( ) ;
115111 assert . doesNotThrow ( ( ) => resolveEffectiveTxParams ( txRequest , { type : txType } ) ) ;
116112 }
117113 } ) ;
118114
119115 it ( 'does not throw for Avalanche cross-chain imports resolved from intent.intentType' , function ( ) {
120- // P-chain and C-chain import intents legitimately carry no recipients —
121- // the wallet imports its own UTXOs and the destination address is the
122- // wallet itself. The intentType lives only on the intent (txParams.type
123- // is unset on the MPC signing call) so the guard must read it from there.
124116 for ( const intentType of [ 'import' , 'importtoc' ] ) {
125117 const txRequest = makeTxRequest ( { intent : { intentType } as any } ) ;
126118 assert . doesNotThrow ( ( ) => resolveEffectiveTxParams ( txRequest , { } ) ) ;
127119 }
128120 } ) ;
129121
130122 it ( 'does not throw when buildParams.type is PascalCase but intent.intentType is lowercase' , function ( ) {
131- // signTransactionTss passes txPrebuild.buildParams as txParams. Prebuild uses
132- // type: 'Import' while WP stores intentType: 'import' on the txRequest.
133123 const txRequest = makeTxRequest ( { intent : { intentType : 'import' , recipients : [ ] } as any } ) ;
134124 assert . doesNotThrow ( ( ) => resolveEffectiveTxParams ( txRequest , { type : 'Import' , recipients : [ ] } ) ) ;
135125 } ) ;
@@ -182,5 +172,201 @@ describe('recipientUtils', function () {
182172 const result = resolveEffectiveTxParams ( txRequest , txParams ) ;
183173 assert . strictEqual ( result . recipients ?. [ 0 ] . address , '0xcaller' ) ;
184174 } ) ;
175+
176+ it ( 'preserves data field from intent recipients' , function ( ) {
177+ const txRequest = makeTxRequest ( {
178+ intent : {
179+ intentType : 'payment' ,
180+ recipients : [ { address : { address : '0xabc' } , amount : { value : '100' , symbol : 'eth' } , data : '0xdeadbeef' } ] ,
181+ } as any ,
182+ } ) ;
183+ const result = resolveEffectiveTxParams ( txRequest , { } ) ;
184+ assert . strictEqual ( result . recipients ?. [ 0 ] . data , '0xdeadbeef' ) ;
185+ } ) ;
186+
187+ // -------------------------------------------------------------------------
188+ // Regression: WCN-196 / WCI-1110
189+ // resolveEffectiveTxParams was briefly wired into EdDSA signing (PR #9071 /
190+ // a70114f21a) then reverted (96658f105f) because it dropped tokenName when
191+ // mapping intent recipients. SOL token sendMany (e.g. tsol:usdc) requires
192+ // tokenName to derive the ATA address in verifyTransaction — without it the
193+ // comparison always fails with "Tx outputs does not match".
194+ // -------------------------------------------------------------------------
195+
196+ describe ( 'tokenName preservation (WCN-196 regression)' , function ( ) {
197+ it ( 'preserves tokenName from amount.symbol when it differs from chainName' , function ( ) {
198+ const txRequest = makeTxRequest ( {
199+ intent : {
200+ intentType : 'payment' ,
201+ recipients : [
202+ {
203+ address : { address : 'UserWalletAddress111111111111111111111111111' } ,
204+ amount : { value : '1000000' , symbol : 'tsol:usdc' } ,
205+ } ,
206+ ] ,
207+ } as any ,
208+ } ) ;
209+ const result = resolveEffectiveTxParams ( txRequest , { } , 'tsol' ) ;
210+ assert . strictEqual ( result . recipients ?. length , 1 ) ;
211+ assert . strictEqual ( result . recipients ?. [ 0 ] . tokenName , 'tsol:usdc' ) ;
212+ } ) ;
213+
214+ it ( 'does NOT set tokenName when symbol equals chainName (native SOL transfer)' , function ( ) {
215+ const txRequest = makeTxRequest ( {
216+ intent : {
217+ intentType : 'payment' ,
218+ recipients : [
219+ {
220+ address : { address : 'RecipientAddress111111111111111111111111111' } ,
221+ amount : { value : '5000000000' , symbol : 'tsol' } ,
222+ } ,
223+ ] ,
224+ } as any ,
225+ } ) ;
226+ const result = resolveEffectiveTxParams ( txRequest , { } , 'tsol' ) ;
227+ assert . strictEqual ( result . recipients ?. [ 0 ] . tokenName , undefined ) ;
228+ } ) ;
229+
230+ it ( 'prefers tokenData.tokenName over amount.symbol (uses distinct values to verify)' , function ( ) {
231+ const txRequest = makeTxRequest ( {
232+ intent : {
233+ intentType : 'payment' ,
234+ recipients : [
235+ {
236+ address : { address : 'RecipientAddress111111111111111111111111111' } ,
237+ amount : { value : '500000' , symbol : 'tsol:usdc-alt' } ,
238+ tokenData : { tokenType : 'fungible' , tokenQuantity : '500000' , tokenName : 'canonical-token-name' } ,
239+ } ,
240+ ] ,
241+ } as any ,
242+ } ) ;
243+ const result = resolveEffectiveTxParams ( txRequest , { } , 'tsol' ) ;
244+ assert . strictEqual ( result . recipients ?. [ 0 ] . tokenName , 'canonical-token-name' ) ;
245+ } ) ;
246+
247+ it ( 'falls back to amount.symbol when tokenData.tokenName is absent' , function ( ) {
248+ const txRequest = makeTxRequest ( {
249+ intent : {
250+ intentType : 'payment' ,
251+ recipients : [
252+ {
253+ address : { address : 'RecipientAddress111111111111111111111111111' } ,
254+ amount : { value : '200000' , symbol : 'sol:usdc' } ,
255+ tokenData : { tokenType : 'fungible' , tokenQuantity : '200000' } ,
256+ } ,
257+ ] ,
258+ } as any ,
259+ } ) ;
260+ const result = resolveEffectiveTxParams ( txRequest , { } , 'sol' ) ;
261+ assert . strictEqual ( result . recipients ?. [ 0 ] . tokenName , 'sol:usdc' ) ;
262+ } ) ;
263+
264+ it ( 'falls back to amount.symbol when tokenData.tokenName is empty string' , function ( ) {
265+ const txRequest = makeTxRequest ( {
266+ intent : {
267+ intentType : 'payment' ,
268+ recipients : [
269+ {
270+ address : { address : 'RecipientAddress111111111111111111111111111' } ,
271+ amount : { value : '100000' , symbol : 'tsol:usdc' } ,
272+ tokenData : { tokenType : 'fungible' , tokenQuantity : '100000' , tokenName : '' } ,
273+ } ,
274+ ] ,
275+ } as any ,
276+ } ) ;
277+ const result = resolveEffectiveTxParams ( txRequest , { } , 'tsol' ) ;
278+ assert . strictEqual ( result . recipients ?. [ 0 ] . tokenName , 'tsol:usdc' ) ;
279+ } ) ;
280+
281+ it ( 'does NOT set tokenName when chainName is absent (legacy ECDSA callers, no tokenData)' , function ( ) {
282+ const txRequest = makeTxRequest ( {
283+ intent : {
284+ intentType : 'payment' ,
285+ recipients : [ { address : { address : '0xabc' } , amount : { value : '100' , symbol : 'eth' } } ] ,
286+ } as any ,
287+ } ) ;
288+ const result = resolveEffectiveTxParams ( txRequest , { } ) ;
289+ assert . strictEqual ( result . recipients ?. [ 0 ] . tokenName , undefined ) ;
290+ assert . strictEqual ( result . recipients ?. [ 0 ] . address , '0xabc' ) ;
291+ } ) ;
292+
293+ it ( 'preserves tokenData.tokenName when chainName is absent (legacy ECDSA with tokenData)' , function ( ) {
294+ const txRequest = makeTxRequest ( {
295+ intent : {
296+ intentType : 'transferToken' ,
297+ recipients : [
298+ {
299+ address : { address : '0xabc' } ,
300+ amount : { value : '1000' , symbol : 'erc20:usdc' } ,
301+ tokenData : { tokenType : 'fungible' , tokenQuantity : '1000' , tokenName : 'eth:usdc' } ,
302+ } ,
303+ ] ,
304+ } as any ,
305+ } ) ;
306+ const result = resolveEffectiveTxParams ( txRequest , { } ) ;
307+ assert . strictEqual ( result . recipients ?. [ 0 ] . tokenName , 'eth:usdc' ) ;
308+ } ) ;
309+
310+ it ( 'sendMany tsol:usdc: does not throw and preserves tokenName in full round-trip' , function ( ) {
311+ const txRequest = makeTxRequest ( {
312+ intent : {
313+ intentType : 'payment' ,
314+ recipients : [
315+ {
316+ address : { address : 'SolUserWallet1111111111111111111111111111111' } ,
317+ amount : { value : '2000000' , symbol : 'tsol:usdc' } ,
318+ } ,
319+ {
320+ address : { address : 'SolUserWallet2222222222222222222222222222222' } ,
321+ amount : { value : '3000000' , symbol : 'tsol:usdc' } ,
322+ } ,
323+ ] ,
324+ } as any ,
325+ } ) ;
326+ const result = resolveEffectiveTxParams ( txRequest , { } , 'tsol' ) ;
327+ assert . strictEqual ( result . recipients ?. length , 2 ) ;
328+ result . recipients ! . forEach ( ( r ) => assert . strictEqual ( r . tokenName , 'tsol:usdc' ) ) ;
329+ assert . strictEqual ( result . recipients ! [ 0 ] . amount , '2000000' ) ;
330+ assert . strictEqual ( result . recipients ! [ 1 ] . amount , '3000000' ) ;
331+ } ) ;
332+
333+ it ( 'handles mixed native + token recipients correctly' , function ( ) {
334+ const txRequest = makeTxRequest ( {
335+ intent : {
336+ intentType : 'payment' ,
337+ recipients : [
338+ {
339+ address : { address : 'SolNative111111111111111111111111111111111' } ,
340+ amount : { value : '1000000000' , symbol : 'tsol' } ,
341+ } ,
342+ {
343+ address : { address : 'SolToken111111111111111111111111111111111' } ,
344+ amount : { value : '500000' , symbol : 'tsol:usdc' } ,
345+ } ,
346+ ] ,
347+ } as any ,
348+ } ) ;
349+ const result = resolveEffectiveTxParams ( txRequest , { } , 'tsol' ) ;
350+ assert . strictEqual ( result . recipients ! [ 0 ] . tokenName , undefined ) ;
351+ assert . strictEqual ( result . recipients ! [ 1 ] . tokenName , 'tsol:usdc' ) ;
352+ } ) ;
353+ } ) ;
354+
355+ describe ( 'SOL no-recipient intent types' , function ( ) {
356+ it ( 'does not throw for "delegate" (solDelegateIntent)' , function ( ) {
357+ const txRequest = makeTxRequest ( { intent : { intentType : 'delegate' } as any } ) ;
358+ assert . doesNotThrow ( ( ) => resolveEffectiveTxParams ( txRequest , { } ) ) ;
359+ } ) ;
360+
361+ it ( 'does not throw for "deactivate" (solDeactivateIntent)' , function ( ) {
362+ const txRequest = makeTxRequest ( { intent : { intentType : 'deactivate' } as any } ) ;
363+ assert . doesNotThrow ( ( ) => resolveEffectiveTxParams ( txRequest , { } ) ) ;
364+ } ) ;
365+
366+ it ( 'throws for stakingAuthorize — must be validated at coin layer' , function ( ) {
367+ const txRequest = makeTxRequest ( { intent : { intentType : 'stakingAuthorize' } as any } ) ;
368+ assert . throws ( ( ) => resolveEffectiveTxParams ( txRequest , { } ) , InvalidTransactionError ) ;
369+ } ) ;
370+ } ) ;
185371 } ) ;
186372} ) ;
0 commit comments