Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ur-sdk): add command parser #127

Merged
merged 9 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading