UNPKG

micro-eth-signer

Version:

Minimal library for Ethereum transactions, addresses and smart contracts

230 lines (220 loc) 7.98 kB
import { concatBytes } from '@noble/hashes/utils'; import { type ContractInfo, createContract } from '../abi/decoder.ts'; import { default as UNISWAP_V3_ROUTER, UNISWAP_V3_ROUTER_CONTRACT } from '../abi/uniswap-v3.ts'; import { type IWeb3Provider, ethHex } from '../utils.ts'; import * as uni from './uniswap-common.ts'; const ADDRESS_ZERO = '0x0000000000000000000000000000000000000000'; const QUOTER_ADDRESS = '0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6'; const QUOTER_ABI = [ { type: 'function', name: 'quoteExactInput', inputs: [ { name: 'path', type: 'bytes' }, { name: 'amountIn', type: 'uint256' }, ], outputs: [{ name: 'amountOut', type: 'uint256' }], }, { type: 'function', name: 'quoteExactInputSingle', inputs: [ { name: 'tokenIn', type: 'address' }, { name: 'tokenOut', type: 'address' }, { name: 'fee', type: 'uint24' }, { name: 'amountIn', type: 'uint256' }, { name: 'sqrtPriceLimitX96', type: 'uint160' }, ], outputs: [{ name: 'amountOut', type: 'uint256' }], }, { type: 'function', name: 'quoteExactOutput', inputs: [ { name: 'path', type: 'bytes' }, { name: 'amountOut', type: 'uint256' }, ], outputs: [{ name: 'amountIn', type: 'uint256' }], }, { type: 'function', name: 'quoteExactOutputSingle', inputs: [ { name: 'tokenIn', type: 'address' }, { name: 'tokenOut', type: 'address' }, { name: 'fee', type: 'uint24' }, { name: 'amountOut', type: 'uint256' }, { name: 'sqrtPriceLimitX96', type: 'uint160' }, ], outputs: [{ name: 'amountIn', type: 'uint256' }], }, ] as const; export const Fee: Record<string, number> = { LOW: 500, MEDIUM: 3000, HIGH: 10000, }; type Route = { path?: Uint8Array; fee?: number; amountIn?: bigint; amountOut?: bigint; p?: any }; function basePaths(a: string, b: string, exactOutput: boolean = false) { let res: Route[] = []; for (let fee in Fee) res.push({ fee: Fee[fee], p: [a, b] }); const wA = uni.wrapContract(a); const wB = uni.wrapContract(b); const BASES: (ContractInfo & { contract: string })[] = uni.COMMON_BASES.filter( (c) => c && c.contract && c.contract !== wA && c.contract !== wB ) as (ContractInfo & { contract: string })[]; const packFee = (n: string) => Fee[n].toString(16).padStart(6, '0'); for (let c of BASES) { for (let fee1 in Fee) { for (let fee2 in Fee) { let path = [wA, packFee(fee1), c.contract, packFee(fee2), wB].map((i) => ethHex.decode(i)); if (exactOutput) path = path.reverse(); res.push({ path: concatBytes(...path) }); } } } return res; } async function bestPath( net: IWeb3Provider, a: string, b: string, amountIn?: bigint, amountOut?: bigint ) { if ((amountIn && amountOut) || (!amountIn && !amountOut)) throw new Error('uniswapV3.bestPath: provide only one amount'); const quoter = createContract(QUOTER_ABI, net, QUOTER_ADDRESS); let paths = basePaths(a, b, !!amountOut); for (let i of paths) { if (!i.path && !i.fee) continue; const opt = { ...i, tokenIn: a, tokenOut: b, amountIn, amountOut, sqrtPriceLimitX96: 0 }; const method = 'quoteExact' + (amountIn ? 'Input' : 'Output') + (i.path ? '' : 'Single'); // TODO: remove any i[amountIn ? 'amountOut' : 'amountIn'] = (quoter as any)[method].call(opt); } paths = (await uni.awaitDeep(paths, true)) as any; paths = paths.filter((i) => i.amountIn || i.amountOut); paths.sort((a: any, b: any) => Number(amountIn ? b.amountOut - a.amountOut : a.amountIn - b.amountIn) ); if (!paths.length) throw new Error('uniswap: cannot find path'); return paths[0]; } const ROUTER_CONTRACT = createContract(UNISWAP_V3_ROUTER, undefined, UNISWAP_V3_ROUTER_CONTRACT); export type TxOpt = { slippagePercent: number; ttl: number; sqrtPriceLimitX96?: bigint; deadline?: number; fee?: { fee: number; to: string }; }; export function txData( to: string, input: string, output: string, route: Route, amountIn?: bigint, amountOut?: bigint, opt: TxOpt = uni.DEFAULT_SWAP_OPT ): { to: string; value: bigint; data: Uint8Array; allowance: | { token: string; amount: bigint; } | undefined; } { opt = { ...uni.DEFAULT_SWAP_OPT, ...opt }; const err = 'Uniswap v3: '; if (!uni.isValidUniAddr(input)) throw new Error(err + 'invalid input address'); if (!uni.isValidUniAddr(output)) throw new Error(err + 'invalid output address'); if (!uni.isValidEthAddr(to)) throw new Error(err + 'invalid to address'); if (opt.fee && !uni.isValidUniAddr(opt.fee.to)) throw new Error(err + 'invalid fee recepient addresss'); if (input === 'eth' && output === 'eth') throw new Error(err + 'both input and output cannot be eth'); if ((amountIn && amountOut) || (!amountIn && !amountOut)) throw new Error(err + 'specify either amountIn or amountOut, but not both'); if ( (amountIn && !route.amountOut) || (amountOut && !route.amountIn) || (!route.fee && !route.path) ) throw new Error(err + 'invalid route'); if (route.path && opt.sqrtPriceLimitX96) throw new Error(err + 'sqrtPriceLimitX96 on multi-hop trade'); const deadline = opt.deadline || Math.floor(Date.now() / 1000); // flags for whether funds should be send first to the router const routerMustCustody = output === 'eth' || !!opt.fee; // TODO: remove "as bigint" let args = { ...route, tokenIn: uni.wrapContract(input), tokenOut: uni.wrapContract(output), recipient: routerMustCustody ? ADDRESS_ZERO : to, deadline, amountIn: (amountIn || route.amountIn) as bigint, amountOut: (amountOut || route.amountOut) as bigint, sqrtPriceLimitX96: opt.sqrtPriceLimitX96 || BigInt(0), amountInMaximum: undefined as bigint | undefined, amountOutMinimum: undefined as bigint | undefined, }; args.amountInMaximum = uni.addPercent(args.amountIn, opt.slippagePercent); args.amountOutMinimum = uni.addPercent(args.amountOut, -opt.slippagePercent); const method = ('exact' + (amountIn ? 'Input' : 'Output') + (!args.path ? 'Single' : '')) as | 'exactInput' | 'exactOutput' | 'exactInputSingle' | 'exactOutputSingle'; // TODO: remove unknown const calldatas = [(ROUTER_CONTRACT[method].encodeInput as (v: unknown) => Uint8Array)(args)]; if (input === 'eth' && amountOut) calldatas.push(ROUTER_CONTRACT['refundETH'].encodeInput()); // unwrap if (routerMustCustody) { calldatas.push( (ROUTER_CONTRACT as any)[ (output === 'eth' ? 'unwrapWETH9' : 'sweepToken') + (opt.fee ? 'WithFee' : '') ].encodeInput({ token: uni.wrapContract(output), amountMinimum: args.amountOutMinimum, recipient: to, feeBips: opt.fee && opt.fee.fee * 10000, feeRecipient: opt.fee && opt.fee.to, }) ); } const data = calldatas.length === 1 ? calldatas[0] : ROUTER_CONTRACT['multicall'].encodeInput(calldatas); const value = input === 'eth' ? (amountIn ? amountIn : args.amountInMaximum) : BigInt(0); const allowance = input !== 'eth' ? { token: input, amount: amountIn ? amountIn : args.amountInMaximum } : undefined; return { to: UNISWAP_V3_ROUTER_CONTRACT, value, data, allowance }; } // Here goes Exchange API. Everything above is SDK. export default class UniswapV3 extends uni.UniswapAbstract { name = 'Uniswap V3'; contract: string = UNISWAP_V3_ROUTER_CONTRACT; bestPath(fromCoin: string, toCoin: string, inputAmount: bigint): Promise<Route> { return bestPath(this.net, fromCoin, toCoin, inputAmount); } txData( toAddress: string, fromCoin: string, toCoin: string, path: any, inputAmount?: bigint, outputAmount?: bigint, opt: uni.SwapOpt = uni.DEFAULT_SWAP_OPT ): any { return txData(toAddress, fromCoin, toCoin, path, inputAmount, outputAmount, { ...uni.DEFAULT_SWAP_OPT, ...opt, }); } }