11import type { Account , Address , Client , Hex } from 'viem'
2- import { encodeFunctionData , isAddressEqual , parseEventLogs } from 'viem'
2+ import { decodeFunctionData , encodeFunctionData , isAddressEqual , parseEventLogs } from 'viem'
33import {
44 call ,
55 prepareTransactionRequest ,
@@ -10,7 +10,7 @@ import {
1010 signTransaction ,
1111 waitForTransactionReceipt ,
1212} from 'viem/actions'
13- import { Transaction } from 'viem/tempo'
13+ import { Abis , Addresses , Transaction } from 'viem/tempo'
1414
1515import { BadRequestError , VerificationFailedError } from '../../../Errors.js'
1616import * as FeePayer from '../../internal/fee-payer.js'
@@ -401,6 +401,7 @@ export type ChannelTransactionOptions = {
401401
402402type ParsedPrecompileCredentialTransaction = {
403403 call : Transaction . TransactionTempo [ 'calls' ] [ number ] & { data : Hex ; to : Address }
404+ prefixCalls : readonly Transaction . TransactionTempo [ 'calls' ] [ number ] [ ]
404405 transaction : ReturnType < ( typeof Transaction ) [ 'deserialize' ] >
405406}
406407
@@ -418,11 +419,11 @@ function parsePrecompileCredentialTransaction(parameters: {
418419 serializedTransaction as Transaction . TransactionSerializedTempo ,
419420 )
420421 const calls = transaction . calls
421- if ( calls . length !== 1 )
422+ if ( calls . length !== 1 && calls . length !== 3 )
422423 throw new VerificationFailedError ( {
423- reason : `TIP-1034 ${ label } transaction must contain exactly one call` ,
424+ reason : `TIP-1034 ${ label } transaction must contain one management call, optionally preceded by an auto-swap ` ,
424425 } )
425- const call = calls [ 0 ] !
426+ const call = calls . at ( - 1 ) !
426427 if ( ! call . to || ! isAddressEqual ( call . to , escrowContract ) )
427428 throw new VerificationFailedError ( {
428429 reason : `TIP-1034 ${ label } transaction targets the wrong address` ,
@@ -431,7 +432,88 @@ function parsePrecompileCredentialTransaction(parameters: {
431432 throw new VerificationFailedError ( {
432433 reason : `TIP-1034 ${ label } transaction is missing calldata` ,
433434 } )
434- return { transaction, call : { ...call , data : call . data , to : call . to } }
435+ return {
436+ transaction,
437+ call : { ...call , data : call . data , to : call . to } ,
438+ prefixCalls : calls . slice ( 0 , - 1 ) ,
439+ }
440+ }
441+
442+ function validateAutoSwapPrefix ( parameters : {
443+ amountOut : bigint
444+ currency : Address
445+ label : 'open' | 'topUp'
446+ prefixCalls : readonly Transaction . TransactionTempo [ 'calls' ] [ number ] [ ]
447+ } ) {
448+ const { amountOut, currency, label, prefixCalls } = parameters
449+ if ( prefixCalls . length === 0 ) return
450+
451+ const fail = ( reason : string ) : never => {
452+ throw new VerificationFailedError ( { reason : `TIP-1034 ${ label } auto-swap ${ reason } ` } )
453+ }
454+ if ( prefixCalls . length !== 2 ) fail ( 'must contain exactly approve and swap calls' )
455+
456+ const approveCall = prefixCalls [ 0 ] !
457+ const swapCall = prefixCalls [ 1 ] !
458+ const approveTo = approveCall . to
459+ const approveData = approveCall . data
460+ const swapTo = swapCall . to
461+ const swapData = swapCall . data
462+ if ( ! approveTo || ! approveData || ! swapTo || ! swapData )
463+ fail ( 'call is missing a target or calldata' )
464+ const checkedApproveTo = approveTo as Address
465+ const checkedApproveData = approveData as Hex
466+ const checkedSwapTo = swapTo as Address
467+ const checkedSwapData = swapData as Hex
468+ if ( ( approveCall . value ?? 0n ) !== 0n || ( swapCall . value ?? 0n ) !== 0n )
469+ fail ( 'calls must not transfer native value' )
470+ if ( ! isAddressEqual ( checkedSwapTo , Addresses . stablecoinDex ) ) fail ( 'targets the wrong DEX' )
471+
472+ const approve = ( ( ) => {
473+ try {
474+ return decodeFunctionData ( { abi : Abis . tip20 , data : checkedApproveData } )
475+ } catch {
476+ return fail ( 'approval calldata is invalid' )
477+ }
478+ } ) ( )
479+ const swap = ( ( ) => {
480+ try {
481+ return decodeFunctionData ( { abi : Abis . stablecoinDex , data : checkedSwapData } )
482+ } catch {
483+ return fail ( 'swap calldata is invalid' )
484+ }
485+ } ) ( )
486+ if ( approve . functionName !== 'approve' || swap . functionName !== 'swapExactAmountOut' )
487+ fail ( 'must contain approve followed by swapExactAmountOut' )
488+
489+ const [ spender , approvedAmount ] = approve . args as readonly [ Address , bigint ]
490+ const [ tokenIn , tokenOut , swapAmountOut , maxAmountIn ] = swap . args as readonly [
491+ Address ,
492+ Address ,
493+ bigint ,
494+ bigint ,
495+ ]
496+ if ( ! isAddressEqual ( checkedApproveTo , tokenIn ) ) fail ( 'approval token does not match swap input' )
497+ if ( ! isAddressEqual ( spender , Addresses . stablecoinDex ) ) fail ( 'approval spender is not the DEX' )
498+ if ( approvedAmount !== maxAmountIn ) fail ( 'approval amount does not match swap maximum input' )
499+ if ( ! isAddressEqual ( tokenOut , currency ) ) fail ( 'output token does not match channel currency' )
500+ if ( swapAmountOut !== amountOut ) fail ( 'output amount does not match channel deposit' )
501+ if ( isAddressEqual ( tokenIn , tokenOut ) ) fail ( 'input and output tokens must differ' )
502+
503+ const canonicalApprove = encodeFunctionData ( {
504+ abi : Abis . tip20 ,
505+ functionName : 'approve' ,
506+ args : [ spender , approvedAmount ] ,
507+ } )
508+ const canonicalSwap = encodeFunctionData ( {
509+ abi : Abis . stablecoinDex ,
510+ functionName : 'swapExactAmountOut' ,
511+ args : [ tokenIn , tokenOut , swapAmountOut , maxAmountIn ] ,
512+ } )
513+ if ( checkedApproveData . toLowerCase ( ) !== canonicalApprove . toLowerCase ( ) )
514+ fail ( 'approval calldata is not canonical' )
515+ if ( checkedSwapData . toLowerCase ( ) !== canonicalSwap . toLowerCase ( ) )
516+ fail ( 'swap calldata is not canonical' )
435517}
436518
437519async function simulateTempoTransaction ( client : Client , request : unknown ) {
@@ -796,7 +878,7 @@ export type BroadcastOpenTransactionParameters = {
796878export async function broadcastOpenTransaction (
797879 parameters : BroadcastOpenTransactionParameters ,
798880) : Promise < BroadcastOpenTransactionResult > {
799- const { transaction, call } = parsePrecompileCredentialTransaction ( {
881+ const { transaction, call, prefixCalls } = parsePrecompileCredentialTransaction ( {
800882 escrowContract : parameters . escrowContract ,
801883 feePayer : parameters . feePayer ,
802884 label : 'open' ,
@@ -812,6 +894,12 @@ export async function broadcastOpenTransaction(
812894 authorizedSigner : parameters . expectedAuthorizedSigner ,
813895 } ,
814896 } )
897+ validateAutoSwapPrefix ( {
898+ amountOut : open . deposit ,
899+ currency : parameters . expectedCurrency ,
900+ label : 'open' ,
901+ prefixCalls,
902+ } )
815903 const descriptor = ChannelOps . descriptorFromOpen ( {
816904 chainId : parameters . chainId ,
817905 escrow : parameters . escrowContract ,
@@ -920,7 +1008,7 @@ export type BroadcastTopUpTransactionParameters = {
9201008export async function broadcastTopUpTransaction (
9211009 parameters : BroadcastTopUpTransactionParameters ,
9221010) : Promise < BroadcastTopUpTransactionResult > {
923- const { transaction, call } = parsePrecompileCredentialTransaction ( {
1011+ const { transaction, call, prefixCalls } = parsePrecompileCredentialTransaction ( {
9241012 escrowContract : parameters . escrowContract ,
9251013 feePayer : parameters . feePayer ,
9261014 label : 'topUp' ,
@@ -933,6 +1021,12 @@ export async function broadcastTopUpTransaction(
9331021 additionalDeposit : parameters . additionalDeposit ,
9341022 } ,
9351023 } )
1024+ validateAutoSwapPrefix ( {
1025+ amountOut : parameters . additionalDeposit ,
1026+ currency : parameters . expectedCurrency ,
1027+ label : 'topUp' ,
1028+ prefixCalls,
1029+ } )
9361030 const receipt = await sendCredentialTransaction ( {
9371031 challengeExpires : parameters . challengeExpires ,
9381032 chainId : parameters . chainId ,
0 commit comments