@@ -63,6 +63,19 @@ function sponsoredFeeExposure(credential: string) {
6363 return transaction . gas * transaction . maxFeePerGas
6464}
6565
66+ function toRawRecoveryId ( serializedTransaction : Hex . Hex ) {
67+ const transaction = TxEnvelopeTempo . deserialize (
68+ serializedTransaction as TxEnvelopeTempo . Serialized ,
69+ )
70+ if ( transaction . signature ?. type !== 'secp256k1' )
71+ throw new Error ( 'expected a secp256k1 transaction signature' )
72+ const { yParity } = transaction . signature . signature
73+ const canonicalRecoveryId = yParity === 0 ? '1b' : '1c'
74+ if ( ! serializedTransaction . endsWith ( canonicalRecoveryId ) )
75+ throw new Error ( 'expected an Electrum-encoded recovery ID' )
76+ return `${ serializedTransaction . slice ( 0 , - 2 ) } 0${ yParity } ` as Hex . Hex
77+ }
78+
6679function tokenTransferCall ( parameters : viem_token . transfer . Args ) {
6780 return Actions . token . transfer . call ( client , parameters )
6881}
@@ -1092,6 +1105,102 @@ describe('tempo', () => {
10921105 } )
10931106
10941107 describe ( 'intent: charge; type: transaction; via Mppx' , ( ) => {
1108+ test . each ( [
1109+ {
1110+ name : 'confirmed' ,
1111+ rpcMethod : 'eth_sendRawTransactionSync' ,
1112+ waitForConfirmation : true ,
1113+ } ,
1114+ {
1115+ name : 'optimistic' ,
1116+ rpcMethod : 'eth_sendRawTransaction' ,
1117+ waitForConfirmation : false ,
1118+ } ,
1119+ ] as const ) (
1120+ 'behavior: canonicalizes raw recovery IDs for $name broadcasts' ,
1121+ async ( { rpcMethod, waitForConfirmation } ) => {
1122+ const broadcastTransactions : Hex . Hex [ ] = [ ]
1123+ const interceptingClient = createClient ( {
1124+ account : accounts [ 0 ] ,
1125+ chain : client . chain ,
1126+ transport : custom ( {
1127+ async request ( request ) {
1128+ if ( request . method === rpcMethod )
1129+ broadcastTransactions . push ( request . params [ 0 ] as Hex . Hex )
1130+ return client . transport . request ( request )
1131+ } ,
1132+ } ) ,
1133+ } )
1134+ const canonicalServer = Mppx_server . create ( {
1135+ methods : [
1136+ tempo_server . charge ( {
1137+ account : accounts [ 0 ] ,
1138+ currency : asset ,
1139+ getClient : ( ) => interceptingClient ,
1140+ waitForConfirmation,
1141+ } ) ,
1142+ ] ,
1143+ realm,
1144+ secretKey,
1145+ } )
1146+ const mppx = Mppx_client . create ( {
1147+ polyfill : false ,
1148+ methods : [
1149+ tempo_client ( {
1150+ account : accounts [ 1 ] ,
1151+ getClient : ( ) => client ,
1152+ } ) ,
1153+ ] ,
1154+ } )
1155+ const httpServer = await Http . createServer ( async ( req , res ) => {
1156+ const result = await Mppx_server . toNodeListener (
1157+ canonicalServer . charge ( {
1158+ amount : '1' ,
1159+ currency : asset ,
1160+ recipient : accounts [ 0 ] . address ,
1161+ } ) ,
1162+ ) ( req , res )
1163+ if ( result . status === 402 ) return
1164+ res . end ( 'OK' )
1165+ } )
1166+
1167+ try {
1168+ const challengeResponse = await fetch ( httpServer . url )
1169+ const canonicalCredential = await mppx . createCredential ( challengeResponse )
1170+ const credential = Credential . deserialize < {
1171+ signature : Hex . Hex
1172+ type : 'transaction'
1173+ } > ( canonicalCredential )
1174+ const canonicalTransaction = credential . payload . signature
1175+ const submittedTransaction = toRawRecoveryId ( canonicalTransaction )
1176+ const rawCredential = Credential . serialize ( {
1177+ ...credential ,
1178+ payload : { ...credential . payload , signature : submittedTransaction } ,
1179+ } )
1180+
1181+ expect ( [ '00' , '01' ] ) . toContain ( submittedTransaction . slice ( - 2 ) )
1182+ expect ( keccak256 ( submittedTransaction ) ) . not . toBe ( keccak256 ( canonicalTransaction ) )
1183+
1184+ const response = await fetch ( httpServer . url , {
1185+ headers : { Authorization : rawCredential } ,
1186+ } )
1187+ expect ( response . status ) . toBe ( 200 )
1188+ expect ( broadcastTransactions ) . toEqual ( [ canonicalTransaction ] )
1189+
1190+ const receipt = Receipt . fromResponse ( response )
1191+ expect ( receipt . reference ) . toBe ( keccak256 ( canonicalTransaction ) )
1192+
1193+ const replay = await fetch ( httpServer . url , {
1194+ headers : { Authorization : canonicalCredential } ,
1195+ } )
1196+ expect ( replay . status ) . toBe ( 402 )
1197+ expect ( broadcastTransactions ) . toHaveLength ( 1 )
1198+ } finally {
1199+ httpServer . close ( )
1200+ }
1201+ } ,
1202+ )
1203+
10951204 test ( 'behavior: accepts a pushed machine-token settlement' , async ( ) => {
10961205 const hash = `0x${ '12' . repeat ( 32 ) } ` as Hex . Hex
10971206 const memo = `0x${ 'ab' . repeat ( 32 ) } ` as Hex . Hex
0 commit comments