@@ -424,6 +424,156 @@ describe("buildFromIntent", function () {
424424 } ) ;
425425 } ) ;
426426
427+ describe ( "payment intent — SPL token transfer" , function ( ) {
428+ // USDC mint address on mainnet
429+ const usdcMint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" ;
430+ const recipient = "FKjSjCqByQRwSzZoMXA7bKnDbJe41YgJTHFFzBeC42bH" ;
431+
432+ it ( "should build an SPL token transfer with tokenAddress + decimalPlaces" , function ( ) {
433+ const intent = {
434+ intentType : "payment" ,
435+ recipients : [
436+ {
437+ address : { address : recipient } ,
438+ amount : { value : 1000000n , symbol : "sol:usdc" } ,
439+ tokenAddress : usdcMint ,
440+ decimalPlaces : 6 ,
441+ } ,
442+ ] ,
443+ } ;
444+
445+ const result = buildFromIntent ( intent , {
446+ feePayer,
447+ nonce : { type : "blockhash" , value : blockhash } ,
448+ } ) ;
449+
450+ assert ( result . transaction instanceof Transaction , "Should return Transaction object" ) ;
451+ assert . equal ( result . generatedKeypairs . length , 0 , "Should not generate keypairs" ) ;
452+
453+ const parsed = parseTransaction ( result . transaction ) ;
454+
455+ const createAta = parsed . instructionsData . find (
456+ ( i : any ) => i . type === "CreateAssociatedTokenAccount" ,
457+ ) ;
458+ assert ( createAta , "Should have CreateAssociatedTokenAccount instruction" ) ;
459+
460+ const tokenTransfer = parsed . instructionsData . find ( ( i : any ) => i . type === "TokenTransfer" ) ;
461+ assert ( tokenTransfer , "Should have TokenTransfer instruction" ) ;
462+ assert . equal ( ( tokenTransfer as any ) . tokenAddress , usdcMint , "Token mint should match" ) ;
463+ assert . equal ( ( tokenTransfer as any ) . amount , BigInt ( 1000000 ) , "Token amount should match" ) ;
464+ } ) ;
465+
466+ it ( "should build native SOL transfer (regression — no token fields)" , function ( ) {
467+ const intent = {
468+ intentType : "payment" ,
469+ recipients : [
470+ {
471+ address : { address : recipient } ,
472+ amount : { value : 1000000n } ,
473+ } ,
474+ ] ,
475+ } ;
476+
477+ const result = buildFromIntent ( intent , {
478+ feePayer,
479+ nonce : { type : "blockhash" , value : blockhash } ,
480+ } ) ;
481+
482+ const parsed = parseTransaction ( result . transaction ) ;
483+
484+ const transfer = parsed . instructionsData . find ( ( i : any ) => i . type === "Transfer" ) ;
485+ assert ( transfer , "Should have native SOL Transfer instruction" ) ;
486+
487+ const tokenTransfer = parsed . instructionsData . find ( ( i : any ) => i . type === "TokenTransfer" ) ;
488+ assert ( ! tokenTransfer , "Should NOT have TokenTransfer instruction" ) ;
489+ } ) ;
490+
491+ it ( "should error when decimalPlaces is missing for a token transfer" , function ( ) {
492+ const intent = {
493+ intentType : "payment" ,
494+ recipients : [
495+ {
496+ address : { address : recipient } ,
497+ amount : { value : 1000000n } ,
498+ tokenAddress : usdcMint ,
499+ // decimalPlaces intentionally omitted
500+ } ,
501+ ] ,
502+ } ;
503+
504+ assert . throws ( ( ) => {
505+ buildFromIntent ( intent , {
506+ feePayer,
507+ nonce : { type : "blockhash" , value : blockhash } ,
508+ } ) ;
509+ } , / T o k e n t r a n s f e r r e q u i r e s d e c i m a l P l a c e s / ) ;
510+ } ) ;
511+
512+ it ( "should build mixed payment (native SOL + SPL token recipients)" , function ( ) {
513+ const intent = {
514+ intentType : "payment" ,
515+ recipients : [
516+ {
517+ address : { address : recipient } ,
518+ amount : { value : 2000000n } ,
519+ } ,
520+ {
521+ address : { address : "5ZWgXcyqrrNpQHCme5SdC5hCeYb2o3fEJhF7Gok3bTVN" } ,
522+ amount : { value : 100000n } ,
523+ tokenAddress : usdcMint ,
524+ decimalPlaces : 6 ,
525+ } ,
526+ ] ,
527+ } ;
528+
529+ const result = buildFromIntent ( intent , {
530+ feePayer,
531+ nonce : { type : "blockhash" , value : blockhash } ,
532+ } ) ;
533+
534+ const parsed = parseTransaction ( result . transaction ) ;
535+
536+ const solTransfer = parsed . instructionsData . find ( ( i : any ) => i . type === "Transfer" ) ;
537+ assert ( solTransfer , "Should have native SOL Transfer instruction" ) ;
538+
539+ const tokenTransfer = parsed . instructionsData . find ( ( i : any ) => i . type === "TokenTransfer" ) ;
540+ assert ( tokenTransfer , "Should have SPL TokenTransfer instruction" ) ;
541+ } ) ;
542+
543+ it ( "parse round-trip: build token transfer then verify parsed output" , function ( ) {
544+ const intent = {
545+ intentType : "payment" ,
546+ recipients : [
547+ {
548+ address : { address : recipient } ,
549+ amount : { value : 1234567n } ,
550+ tokenAddress : usdcMint ,
551+ decimalPlaces : 6 ,
552+ } ,
553+ ] ,
554+ } ;
555+
556+ const { transaction } = buildFromIntent ( intent , {
557+ feePayer,
558+ nonce : { type : "blockhash" , value : blockhash } ,
559+ } ) ;
560+
561+ // Parse round-trip via bytes
562+ const bytes = transaction . toBytes ( ) ;
563+ const txFromBytes = Transaction . fromBytes ( bytes ) ;
564+ const parsed = parseTransaction ( txFromBytes ) ;
565+
566+ const tokenTransfer = parsed . instructionsData . find ( ( i : any ) => i . type === "TokenTransfer" ) ;
567+ assert ( tokenTransfer , "Parsed output should have TokenTransfer" ) ;
568+ assert . equal ( ( tokenTransfer as any ) . tokenAddress , usdcMint , "Mint should survive round-trip" ) ;
569+ assert . equal (
570+ ( tokenTransfer as any ) . amount ,
571+ BigInt ( 1234567 ) ,
572+ "Amount should survive round-trip" ,
573+ ) ;
574+ } ) ;
575+ } ) ;
576+
427577 describe ( "error handling" , function ( ) {
428578 it ( "should reject invalid intent type" , function ( ) {
429579 const intent = {
0 commit comments