Skip to content

Commit

Permalink
feat(ur-sdk): add command parser (#127)
Browse files Browse the repository at this point in the history
  • Loading branch information
marktoda authored Oct 2, 2024
1 parent 42e3db2 commit b637f99
Show file tree
Hide file tree
Showing 4 changed files with 500 additions and 23 deletions.
1 change: 1 addition & 0 deletions sdks/universal-router-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export {
WETH_ADDRESS,
UniversalRouterVersion,
} from './utils/constants'
export { CommandParser, UniversalRouterCommand, UniversalRouterCall, Param } from './utils/commandParser'
129 changes: 129 additions & 0 deletions sdks/universal-router-sdk/src/utils/commandParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { ethers } from 'ethers'
import { abi } from '@uniswap/universal-router/artifacts/contracts/UniversalRouter.sol/UniversalRouter.json'
import { Interface } from '@ethersproject/abi'
import { CommandType, COMMAND_ABI_DEFINITION, Subparser } from '../utils/routerCommands'

export type Param = {
readonly name: string
readonly value: any
}

export type UniversalRouterCommand = {
readonly commandName: string
readonly commandType: CommandType
readonly params: readonly Param[]
}

export type UniversalRouterCall = {
readonly commands: readonly UniversalRouterCommand[]
}

export type V3PathItem = {
readonly tokenIn: string
readonly tokenOut: string
readonly fee: number
}

// Parses UniversalRouter commands
export abstract class CommandParser {
public static INTERFACE: Interface = new Interface(abi)

public static parseCalldata(calldata: string): UniversalRouterCall {
const txDescription = CommandParser.INTERFACE.parseTransaction({ data: calldata })
const { commands, inputs } = txDescription.args

const commandTypes = CommandParser.getCommands(commands)

return {
commands: commandTypes.map((commandType: CommandType, i: number) => {
const abiDef = COMMAND_ABI_DEFINITION[commandType]
const rawParams = ethers.utils.defaultAbiCoder.decode(
abiDef.map((command) => command.type),
inputs[i]
)
const params = rawParams.map((param: any, j: number) => {
switch (abiDef[j].subparser) {
case Subparser.V3PathExactIn:
return {
name: abiDef[j].name,
value: parseV3PathExactIn(param),
}
case Subparser.V3PathExactOut:
return {
name: abiDef[j].name,
value: parseV3PathExactOut(param),
}
default:
return {
name: abiDef[j].name,
value: param,
}
}
})

return {
commandName: CommandType[commandType],
commandType,
params,
}
}),
}
}

// parse command types from bytes string
private static getCommands(commands: string): CommandType[] {
const commandTypes = []

for (let i = 2; i < commands.length; i += 2) {
const byte = commands.substring(i, i + 2)
commandTypes.push(parseInt(byte, 16) as CommandType)
}

return commandTypes
}
}

export function parseV3PathExactIn(path: string): readonly V3PathItem[] {
const strippedPath = path.replace('0x', '')
let tokenIn = ethers.utils.getAddress(strippedPath.substring(0, 40))
let loc = 40
const res = []
while (loc < strippedPath.length) {
const feeAndTokenOut = strippedPath.substring(loc, loc + 46)
const fee = parseInt(feeAndTokenOut.substring(0, 6), 16)
const tokenOut = ethers.utils.getAddress(feeAndTokenOut.substring(6, 46))

res.push({
tokenIn,
tokenOut,
fee,
})
tokenIn = tokenOut
loc += 46
}

return res
}

export function parseV3PathExactOut(path: string): readonly V3PathItem[] {
const strippedPath = path.replace('0x', '')
let tokenIn = ethers.utils.getAddress(strippedPath.substring(strippedPath.length - 40))
let loc = strippedPath.length - 86 // 86 = (20 addr + 3 fee + 20 addr) * 2 (for hex characters)
const res = []
while (loc >= 0) {
const feeAndTokenOut = strippedPath.substring(loc, loc + 46)
const tokenOut = ethers.utils.getAddress(feeAndTokenOut.substring(0, 40))
const fee = parseInt(feeAndTokenOut.substring(40, 46), 16)

res.push({
tokenIn,
tokenOut,
fee,
})
tokenIn = tokenOut

loc -= 46
}

return res
}
126 changes: 103 additions & 23 deletions sdks/universal-router-sdk/src/utils/routerCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@ export enum CommandType {
EXECUTE_SUB_PLAN = 0x21,
}

export enum Subparser {
V3PathExactIn,
V3PathExactOut,
}

export type ParamType = {
readonly name: string
readonly type: string
readonly subparser?: Subparser
}

const ALLOW_REVERT_FLAG = 0x80
const REVERTIBLE_COMMANDS = new Set<CommandType>([CommandType.EXECUTE_SUB_PLAN])

Expand All @@ -42,35 +53,99 @@ const PERMIT_BATCH_STRUCT =
const PERMIT2_TRANSFER_FROM_STRUCT = '(address from,address to,uint160 amount,address token)'
const PERMIT2_TRANSFER_FROM_BATCH_STRUCT = PERMIT2_TRANSFER_FROM_STRUCT + '[]'

const ABI_DEFINITION: { [key in CommandType]: string[] } = {
export const COMMAND_ABI_DEFINITION: { [key in CommandType]: readonly ParamType[] } = {
// Batch Reverts
[CommandType.EXECUTE_SUB_PLAN]: ['bytes', 'bytes[]'],
[CommandType.EXECUTE_SUB_PLAN]: [
{ name: 'commands', type: 'bytes' },
{ name: 'inputs', type: 'bytes[]' },
],

// Permit2 Actions
[CommandType.PERMIT2_PERMIT]: [PERMIT_STRUCT, 'bytes'],
[CommandType.PERMIT2_PERMIT_BATCH]: [PERMIT_BATCH_STRUCT, 'bytes'],
[CommandType.PERMIT2_TRANSFER_FROM]: ['address', 'address', 'uint160'],
[CommandType.PERMIT2_TRANSFER_FROM_BATCH]: [PERMIT2_TRANSFER_FROM_BATCH_STRUCT],
[CommandType.PERMIT2_PERMIT]: [
{ name: 'permit', type: PERMIT_STRUCT },
{ name: 'signature', type: 'bytes' },
],
[CommandType.PERMIT2_PERMIT_BATCH]: [
{ name: 'permit', type: PERMIT_BATCH_STRUCT },
{ name: 'signature', type: 'bytes' },
],
[CommandType.PERMIT2_TRANSFER_FROM]: [
{ name: 'token', type: 'address' },
{ name: 'recipient', type: 'address' },
{ name: 'amount', type: 'uint160' },
],
[CommandType.PERMIT2_TRANSFER_FROM_BATCH]: [
{
name: 'transferFrom',
type: PERMIT2_TRANSFER_FROM_BATCH_STRUCT,
},
],

// Uniswap Actions
[CommandType.V3_SWAP_EXACT_IN]: ['address', 'uint256', 'uint256', 'bytes', 'bool'],
[CommandType.V3_SWAP_EXACT_OUT]: ['address', 'uint256', 'uint256', 'bytes', 'bool'],
[CommandType.V2_SWAP_EXACT_IN]: ['address', 'uint256', 'uint256', 'address[]', 'bool'],
[CommandType.V2_SWAP_EXACT_OUT]: ['address', 'uint256', 'uint256', 'address[]', 'bool'],
[CommandType.V4_SWAP]: ['bytes'],
[CommandType.V3_SWAP_EXACT_IN]: [
{ name: 'recipient', type: 'address' },
{ name: 'amountIn', type: 'uint256' },
{ name: 'amountOutMin', type: 'uint256' },
{ name: 'path', subparser: Subparser.V3PathExactIn, type: 'bytes' },
{ name: 'payerIsUser', type: 'bool' },
],
[CommandType.V3_SWAP_EXACT_OUT]: [
{ name: 'recipient', type: 'address' },
{ name: 'amountOut', type: 'uint256' },
{ name: 'amountInMax', type: 'uint256' },
{ name: 'path', subparser: Subparser.V3PathExactOut, type: 'bytes' },
{ name: 'payerIsUser', type: 'bool' },
],
[CommandType.V2_SWAP_EXACT_IN]: [
{ name: 'recipient', type: 'address' },
{ name: 'amountIn', type: 'uint256' },
{ name: 'amountOutMin', type: 'uint256' },
{ name: 'path', type: 'address[]' },
{ name: 'payerIsUser', type: 'bool' },
],
[CommandType.V2_SWAP_EXACT_OUT]: [
{ name: 'recipient', type: 'address' },
{ name: 'amountOut', type: 'uint256' },
{ name: 'amountInMax', type: 'uint256' },
{ name: 'path', type: 'address[]' },
{ name: 'payerIsUser', type: 'bool' },
],
[CommandType.V4_SWAP]: [{ name: 'command', type: 'bytes' }],

// Token Actions and Checks
[CommandType.WRAP_ETH]: ['address', 'uint256'],
[CommandType.UNWRAP_WETH]: ['address', 'uint256'],
[CommandType.SWEEP]: ['address', 'address', 'uint256'],
[CommandType.TRANSFER]: ['address', 'address', 'uint256'],
[CommandType.PAY_PORTION]: ['address', 'address', 'uint256'],
[CommandType.BALANCE_CHECK_ERC20]: ['address', 'address', 'uint256'],
[CommandType.WRAP_ETH]: [
{ name: 'recipient', type: 'address' },
{ name: 'amount', type: 'uint256' },
],
[CommandType.UNWRAP_WETH]: [
{ name: 'recipient', type: 'address' },
{ name: 'amountMin', type: 'uint256' },
],
[CommandType.SWEEP]: [
{ name: 'token', type: 'address' },
{ name: 'recipient', type: 'address' },
{ name: 'amountMin', type: 'uint256' },
],
[CommandType.TRANSFER]: [
{ name: 'token', type: 'address' },
{ name: 'recipient', type: 'address' },
{ name: 'value', type: 'uint256' },
],
[CommandType.PAY_PORTION]: [
{ name: 'token', type: 'address' },
{ name: 'recipient', type: 'address' },
{ name: 'bips', type: 'uint256' },
],
[CommandType.BALANCE_CHECK_ERC20]: [
{ name: 'owner', type: 'address' },
{ name: 'token', type: 'address' },
{ name: 'minBalance', type: 'uint256' },
],

// Position Actions
[CommandType.V3_POSITION_MANAGER_PERMIT]: ['bytes'],
[CommandType.V3_POSITION_MANAGER_CALL]: ['bytes'],
[CommandType.V4_POSITION_CALL]: ['bytes'],
[CommandType.V3_POSITION_MANAGER_PERMIT]: [{ name: 'calldata', type: 'bytes' }],
[CommandType.V3_POSITION_MANAGER_CALL]: [{ name: 'calldata', type: 'bytes' }],
[CommandType.V4_POSITION_CALL]: [{ name: 'calldata', type: 'bytes' }],
}

export class RoutePlanner {
Expand All @@ -82,11 +157,12 @@ export class RoutePlanner {
this.inputs = []
}

addSubPlan(subplan: RoutePlanner): void {
addSubPlan(subplan: RoutePlanner): RoutePlanner {
this.addCommand(CommandType.EXECUTE_SUB_PLAN, [subplan.commands, subplan.inputs], true)
return this
}

addCommand(type: CommandType, parameters: any[], allowRevert = false): void {
addCommand(type: CommandType, parameters: any[], allowRevert = false): RoutePlanner {
let command = createCommand(type, parameters)
this.inputs.push(command.encodedInput)
if (allowRevert) {
Expand All @@ -97,6 +173,7 @@ export class RoutePlanner {
}

this.commands = this.commands.concat(command.type.toString(16).padStart(2, '0'))
return this
}
}

Expand All @@ -109,6 +186,9 @@ export function createCommand(type: CommandType, parameters: any[]): RouterComma
if (type === CommandType.V4_SWAP) {
return { type, encodedInput: parameters[0] }
}
const encodedInput = defaultAbiCoder.encode(ABI_DEFINITION[type], parameters)
const encodedInput = defaultAbiCoder.encode(
COMMAND_ABI_DEFINITION[type].map((abi) => abi.type),
parameters
)
return { type, encodedInput }
}
Loading

0 comments on commit b637f99

Please sign in to comment.