@@ -806,6 +806,228 @@ describe("stackflow agent", () => {
806806 store . close ( ) ;
807807 } ) ;
808808
809+ it ( "rejects incoming transfer requests with non-sequential nonce" , ( ) => {
810+ const dbFile = tempDbFile ( "agent-sign-nonce-gap" ) ;
811+ const store = new AgentStateStore ( { dbFile } ) ;
812+ const contractId = "ST1TESTABC.contract" ;
813+ const pipeKey = {
814+ "principal-1" : "ST1LOCAL" ,
815+ "principal-2" : "ST1OTHER" ,
816+ token : null ,
817+ } ;
818+ const pipeId = buildPipeId ( { contractId, pipeKey } ) ;
819+
820+ store . upsertTrackedPipe ( {
821+ pipeId,
822+ contractId,
823+ pipeKey,
824+ localPrincipal : "ST1LOCAL" ,
825+ counterpartyPrincipal : "ST1OTHER" ,
826+ token : null ,
827+ } ) ;
828+ store . upsertSignatureState ( {
829+ contractId,
830+ pipeKey,
831+ forPrincipal : "ST1LOCAL" ,
832+ withPrincipal : "ST1OTHER" ,
833+ token : null ,
834+ myBalance : "90" ,
835+ theirBalance : "10" ,
836+ nonce : "1" ,
837+ action : "1" ,
838+ actor : "ST1LOCAL" ,
839+ mySignature : "0x" + "11" . repeat ( 65 ) ,
840+ theirSignature : "0x" + "22" . repeat ( 65 ) ,
841+ secret : null ,
842+ validAfter : null ,
843+ beneficialOnly : false ,
844+ } ) ;
845+
846+ const agent = new StackflowAgentService ( {
847+ stateStore : store ,
848+ signer : {
849+ async sip018Sign ( ) {
850+ return "0x" + "44" . repeat ( 65 ) ;
851+ } ,
852+ async submitDispute ( ) {
853+ return { txid : "0x1" } ;
854+ } ,
855+ async callContract ( ) {
856+ return { ok : true } ;
857+ } ,
858+ } ,
859+ network : "devnet" ,
860+ } ) ;
861+
862+ const validation = agent . validateIncomingTransfer ( {
863+ pipeId,
864+ payload : {
865+ contractId,
866+ forPrincipal : "ST1LOCAL" ,
867+ withPrincipal : "ST1OTHER" ,
868+ token : null ,
869+ myBalance : "92" ,
870+ theirBalance : "8" ,
871+ nonce : "3" ,
872+ action : "1" ,
873+ actor : "ST1OTHER" ,
874+ theirSignature : "0x" + "22" . repeat ( 65 ) ,
875+ } ,
876+ } ) ;
877+
878+ expect ( validation . valid ) . toBe ( false ) ;
879+ expect ( validation . reason ) . toBe ( "nonce-not-sequential" ) ;
880+ store . close ( ) ;
881+ } ) ;
882+
883+ it ( "rejects incoming transfer requests that change total pipe balance" , ( ) => {
884+ const dbFile = tempDbFile ( "agent-sign-balance-sum" ) ;
885+ const store = new AgentStateStore ( { dbFile } ) ;
886+ const contractId = "ST1TESTABC.contract" ;
887+ const pipeKey = {
888+ "principal-1" : "ST1LOCAL" ,
889+ "principal-2" : "ST1OTHER" ,
890+ token : null ,
891+ } ;
892+ const pipeId = buildPipeId ( { contractId, pipeKey } ) ;
893+
894+ store . upsertTrackedPipe ( {
895+ pipeId,
896+ contractId,
897+ pipeKey,
898+ localPrincipal : "ST1LOCAL" ,
899+ counterpartyPrincipal : "ST1OTHER" ,
900+ token : null ,
901+ } ) ;
902+ store . upsertSignatureState ( {
903+ contractId,
904+ pipeKey,
905+ forPrincipal : "ST1LOCAL" ,
906+ withPrincipal : "ST1OTHER" ,
907+ token : null ,
908+ myBalance : "90" ,
909+ theirBalance : "10" ,
910+ nonce : "1" ,
911+ action : "1" ,
912+ actor : "ST1LOCAL" ,
913+ mySignature : "0x" + "11" . repeat ( 65 ) ,
914+ theirSignature : "0x" + "22" . repeat ( 65 ) ,
915+ secret : null ,
916+ validAfter : null ,
917+ beneficialOnly : false ,
918+ } ) ;
919+
920+ const agent = new StackflowAgentService ( {
921+ stateStore : store ,
922+ signer : {
923+ async sip018Sign ( ) {
924+ return "0x" + "44" . repeat ( 65 ) ;
925+ } ,
926+ async submitDispute ( ) {
927+ return { txid : "0x1" } ;
928+ } ,
929+ async callContract ( ) {
930+ return { ok : true } ;
931+ } ,
932+ } ,
933+ network : "devnet" ,
934+ } ) ;
935+
936+ const validation = agent . validateIncomingTransfer ( {
937+ pipeId,
938+ payload : {
939+ contractId,
940+ forPrincipal : "ST1LOCAL" ,
941+ withPrincipal : "ST1OTHER" ,
942+ token : null ,
943+ myBalance : "95" ,
944+ theirBalance : "10" ,
945+ nonce : "2" ,
946+ action : "1" ,
947+ actor : "ST1OTHER" ,
948+ theirSignature : "0x" + "22" . repeat ( 65 ) ,
949+ } ,
950+ } ) ;
951+
952+ expect ( validation . valid ) . toBe ( false ) ;
953+ expect ( validation . reason ) . toBe ( "balance-sum-mismatch" ) ;
954+ store . close ( ) ;
955+ } ) ;
956+
957+ it ( "rejects incoming transfer requests with invalid counterparty balance direction" , ( ) => {
958+ const dbFile = tempDbFile ( "agent-sign-balance-direction" ) ;
959+ const store = new AgentStateStore ( { dbFile } ) ;
960+ const contractId = "ST1TESTABC.contract" ;
961+ const pipeKey = {
962+ "principal-1" : "ST1LOCAL" ,
963+ "principal-2" : "ST1OTHER" ,
964+ token : null ,
965+ } ;
966+ const pipeId = buildPipeId ( { contractId, pipeKey } ) ;
967+
968+ store . upsertTrackedPipe ( {
969+ pipeId,
970+ contractId,
971+ pipeKey,
972+ localPrincipal : "ST1LOCAL" ,
973+ counterpartyPrincipal : "ST1OTHER" ,
974+ token : null ,
975+ } ) ;
976+ store . upsertSignatureState ( {
977+ contractId,
978+ pipeKey,
979+ forPrincipal : "ST1LOCAL" ,
980+ withPrincipal : "ST1OTHER" ,
981+ token : null ,
982+ myBalance : "90" ,
983+ theirBalance : "10" ,
984+ nonce : "1" ,
985+ action : "1" ,
986+ actor : "ST1LOCAL" ,
987+ mySignature : "0x" + "11" . repeat ( 65 ) ,
988+ theirSignature : "0x" + "22" . repeat ( 65 ) ,
989+ secret : null ,
990+ validAfter : null ,
991+ beneficialOnly : false ,
992+ } ) ;
993+
994+ const agent = new StackflowAgentService ( {
995+ stateStore : store ,
996+ signer : {
997+ async sip018Sign ( ) {
998+ return "0x" + "44" . repeat ( 65 ) ;
999+ } ,
1000+ async submitDispute ( ) {
1001+ return { txid : "0x1" } ;
1002+ } ,
1003+ async callContract ( ) {
1004+ return { ok : true } ;
1005+ } ,
1006+ } ,
1007+ network : "devnet" ,
1008+ } ) ;
1009+
1010+ const validation = agent . validateIncomingTransfer ( {
1011+ pipeId,
1012+ payload : {
1013+ contractId,
1014+ forPrincipal : "ST1LOCAL" ,
1015+ withPrincipal : "ST1OTHER" ,
1016+ token : null ,
1017+ myBalance : "85" ,
1018+ theirBalance : "15" ,
1019+ nonce : "2" ,
1020+ action : "1" ,
1021+ actor : "ST1OTHER" ,
1022+ theirSignature : "0x" + "22" . repeat ( 65 ) ,
1023+ } ,
1024+ } ) ;
1025+
1026+ expect ( validation . valid ) . toBe ( false ) ;
1027+ expect ( validation . reason ) . toBe ( "balance-direction-invalid" ) ;
1028+ store . close ( ) ;
1029+ } ) ;
1030+
8091031 it ( "rejects incoming transfer requests with pipe key mismatch" , ( ) => {
8101032 const dbFile = tempDbFile ( "agent-sign-pipekey-mismatch" ) ;
8111033 const store = new AgentStateStore ( { dbFile } ) ;
@@ -935,8 +1157,8 @@ describe("stackflow agent", () => {
9351157 forPrincipal : "ST1LOCAL" ,
9361158 withPrincipal : "ST1OTHER" ,
9371159 token : null ,
938- myBalance : "100 " ,
939- theirBalance : "0 " ,
1160+ myBalance : "50 " ,
1161+ theirBalance : "50 " ,
9401162 nonce : "0" ,
9411163 action : "1" ,
9421164 actor : "ST1LOCAL" ,
@@ -968,14 +1190,22 @@ describe("stackflow agent", () => {
9681190 } ) ;
9691191
9701192 expect ( outgoing . actor ) . toBe ( "ST1LOCAL" ) ;
971- expect ( outgoing . myBalance ) . toBe ( "75 " ) ;
972- expect ( outgoing . theirBalance ) . toBe ( "25 " ) ;
1193+ expect ( outgoing . myBalance ) . toBe ( "25 " ) ;
1194+ expect ( outgoing . theirBalance ) . toBe ( "75 " ) ;
9731195 expect ( outgoing . nonce ) . toBe ( "1" ) ;
9741196
9751197 const accepted = await agent . acceptIncomingTransfer ( {
9761198 pipeId,
9771199 payload : {
978- ...outgoing ,
1200+ contractId,
1201+ pipeKey,
1202+ forPrincipal : "ST1LOCAL" ,
1203+ withPrincipal : "ST1OTHER" ,
1204+ token : null ,
1205+ myBalance : "75" ,
1206+ theirBalance : "25" ,
1207+ nonce : "1" ,
1208+ action : "1" ,
9791209 actor : "ST1OTHER" ,
9801210 theirSignature : "0x" + "33" . repeat ( 65 ) ,
9811211 } ,
0 commit comments