UNPKG

@subwallet/invariant-vara-sdk

Version:

<div align="center"> <h1>⚡Invariant protocol⚡</h1> <p> <a href="https://invariant.app/math-spec-vara.pdf">MATH SPEC 📄</a> | <a href="https://discord.gg/VzS3C9wR">DISCORD 🌐</a> | </p> </div>

551 lines (550 loc) 23 kB
import { GearApi } from '@gear-js/api'; import * as wasmSerializer from './wasm-serializer.js'; import { _calculateFee, _newFeeTier, _newPoolKey, _calculateAmountDelta, _getLiquidityByX, _getLiquidityByY, _calculateTick, _isTokenX, getPercentageDenominator, getSqrtPriceDenominator, _getMinSqrtPrice, _getMinTick, _getMaxChunk, _getMaxSqrtPrice, _getMaxTick, _toFeeGrowth, _toFixedPoint, _toLiquidity, _toPercentage, _toPrice, _toSecondsPerLiquidity, _toSqrtPrice, _toTokenAmount, _simulateInvariantSwap, tickIndexToPosition, _positionToTick, _alignTickToSpacing, _calculateSqrtPrice } from '@subwallet/invariant-vara-sdk-wasm'; import { TypeRegistry } from '@polkadot/types'; import { InvariantEvent, InvariantError } from './schema.js'; import { Network } from './network.js'; import { CONCENTRATION_FACTOR, LOCAL, MAINNET, MAX_SWAP_STEPS, TESTNET } from './consts.js'; export const initGearApi = async (network) => { let address; switch (network) { case Network.Local: address = LOCAL; break; case Network.Testnet: address = TESTNET; break; case Network.Mainnet: address = MAINNET; break; default: throw new Error('Network unknown'); } const gearApi = await GearApi.create({ providerAddress: address }); const [chain, nodeName, nodeVersion] = await Promise.all([ gearApi.chain(), gearApi.nodeName(), gearApi.nodeVersion() ]); console.log(`You are connected to chain ${chain} using ${nodeName} v${nodeVersion}`); return gearApi; }; // returns usnub function export const subscribeToNewHeads = async (api) => { return await api.blocks.subscribeNewHeads(header => { console.log(`New block with number: ${header.number.toNumber()} and hash: ${header.hash.toHex()}`); }); }; let nodeModules; // This is necessary to avoid import issues on the fronted const loadNodeModules = async () => { if (typeof window !== 'undefined') { throw new Error('cannot load node modules in a browser environment'); } await import('./node.js') .then(node => { nodeModules = node; }) .catch(error => { console.error('error while loading node modules:', error); }); }; export const getWasm = async (contractName) => { await loadNodeModules(); const __dirname = new URL('.', import.meta.url).pathname; return nodeModules.readFile(nodeModules.join(__dirname, `../contracts/${contractName}/${contractName}.opt.wasm`)); }; export const createTypeByName = (meta, type, payload) => { return meta.createType(meta.getTypeIndexByName(type), payload); }; export const integerSafeCast = (value) => { if (value > BigInt(Number.MAX_SAFE_INTEGER) || value < BigInt(Number.MIN_SAFE_INTEGER)) { throw new Error('Integer value is outside the safe range for Numbers'); } return Number(value); }; export const unwrapResult = (result) => { if ('ok' in result) { return result.ok; } else if (result.err) { throw new Error(result.err); } else { throw new Error('Invalid Result type'); } }; const convertFieldsToBigInt = (returnedObject, exclude) => { for (const [key, value] of Object.entries(returnedObject)) { if (exclude?.includes(key)) { continue; } if (typeof value === 'number' || typeof value === 'string') { returnedObject[key] = BigInt(value); } } return returnedObject; }; export const convertTick = (tick) => { return convertFieldsToBigInt(tick); }; export const convertLiquidityTick = (tick) => { return convertTick(tick); }; export const convertPositionTick = (tick) => { return convertTick(tick); }; export const convertFeeTier = (feeTier) => { return convertFieldsToBigInt(feeTier); }; export const convertPoolKey = (poolKey) => { poolKey.feeTier = convertFeeTier(poolKey.feeTier); return poolKey; }; export const convertPool = (pool) => { return convertFieldsToBigInt(pool, ['currentIndex', 'feeReceiver']); }; export const convertPosition = (position) => { position = convertFieldsToBigInt(position, ['poolKey']); position.poolKey = convertPoolKey(position.poolKey); return position; }; export const convertPositions = (positions) => { positions = positions.map(([pool, positions]) => { pool = convertPool(pool); positions = positions.map(([position, index]) => { return [convertPosition(position), index]; }); return [pool, positions]; }); return positions; }; export const convertPositionCreatedEvent = (positionEvent) => { positionEvent = convertFieldsToBigInt(positionEvent, ['address', 'poolKey']); positionEvent.poolKey = convertPoolKey(positionEvent.poolKey); return positionEvent; }; export const convertPositionRemovedEvent = (positionEvent) => { positionEvent = convertFieldsToBigInt(positionEvent, ['address', 'poolKey']); positionEvent.poolKey = convertPoolKey(positionEvent.poolKey); return positionEvent; }; export const convertSwapEvent = (swapEvent) => { swapEvent = convertFieldsToBigInt(swapEvent, ['address', 'poolKey']); swapEvent.poolKey = convertPoolKey(swapEvent.poolKey); return swapEvent; }; export const convertCrossTickEvent = (crossTickEvent) => { crossTickEvent = convertFieldsToBigInt(crossTickEvent, ['address', 'indexes', 'poolKey']); crossTickEvent.poolKey = convertPoolKey(crossTickEvent.poolKey); crossTickEvent.indexes = crossTickEvent.indexes.map((index) => BigInt(index)); return crossTickEvent; }; export const convertCalculateSwapResult = (calculateSwapResult) => { calculateSwapResult = convertFieldsToBigInt(calculateSwapResult, ['pool', 'ticks']); calculateSwapResult.pool = convertPool(calculateSwapResult.pool); calculateSwapResult.ticks = calculateSwapResult.ticks.map(convertTick); return calculateSwapResult; }; export const convertQuoteResult = (quoteResult) => { quoteResult = convertFieldsToBigInt(quoteResult, ['ticks']); quoteResult.ticks = quoteResult.ticks.map(convertTick); return quoteResult; }; export class TransactionWrapper { txBuilder; decodeCallback = null; validateCallback = null; constructor(txBuilder) { this.txBuilder = txBuilder; } async signAndSend() { try { const { response } = await this.txBuilder.signAndSend(); if (this.decodeCallback) { return this.decodeCallback(await response()); } return await response(); } catch (e) { const message = e.message; if (message) { throw e; } else { throw JSON.stringify(e); } } } withAccount(signer) { this.txBuilder.withAccount(signer); return this; } withDecode(decodeFn) { this.decodeCallback = decodeFn; return this; } withValidate(validateFn) { this.validateCallback = validateFn; return this; } get extrinsic() { return this.txBuilder._tx; } get validation() { return this.validateCallback; } } export const validateFungibleTokenResponse = (message) => { const registry = new TypeRegistry(); const json = registry.createType('(String, String, bool', message.data.message.payload); return json[2].isTrue ? null : 'Token response invalid'; }; const validateInvariantSingleTransfer = (message) => { try { const registry = new TypeRegistry(); registry.createType('(String, String, U256)', message.data.message.payload); } catch (e) { // this may happen if the gas runs out during reply handling return 'Deposit response invalid'; } return null; }; export const validateInvariantSingleDeposit = validateInvariantSingleTransfer; export const validateInvariantSingleWithdraw = validateInvariantSingleTransfer; export const validateInvariantVaraDeposit = validateInvariantSingleTransfer; export const validateInvariantVaraWithdraw = validateInvariantSingleTransfer; const validateInvariantPairTransfer = (message) => { try { const registry = new TypeRegistry(); registry.createType('(String, String, (U256, U256))', message.data.message.payload); } catch (e) { // this may happen if the gas runs out during reply handling return 'Deposit response invalid'; } return null; }; export const validateInvariantPairDeposit = validateInvariantPairTransfer; export const validateInvariantPairWithdraw = validateInvariantPairTransfer; export const decodeEvent = (registry, payload, prefix) => { let type; let convertFunction; switch (prefix) { case InvariantEvent.PositionCreatedEvent: type = '(String, String, {"timestamp":"u64","address":"[u8;32]","poolKey":"PoolKey","liquidityDelta":"Liquidity","lowerTick":"i32","upperTick":"i32","sqrtPrice":"SqrtPrice"})'; convertFunction = convertPositionCreatedEvent; break; case InvariantEvent.PositionRemovedEvent: type = '(String, String, {"timestamp":"u64","address":"[u8;32]","poolKey":"PoolKey","liquidityDelta":"Liquidity","lowerTick":"i32","upperTick":"i32","sqrtPrice":"SqrtPrice"})'; convertFunction = convertPositionRemovedEvent; break; case InvariantEvent.CrossTickEvent: type = '(String, String, {"timestamp":"u64","address":"[u8;32]","poolKey":"PoolKey","indexes":"Vec<i32>"})'; convertFunction = convertCrossTickEvent; break; case InvariantEvent.SwapEvent: type = '(String, String, {"timestamp":"u64","address":"[u8;32]","poolKey":"PoolKey","amountIn":"TokenAmount","amountOut":"TokenAmount","fee":"TokenAmount","startSqrtPrice":"SqrtPrice","targetSqrtPrice":"SqrtPrice","xToY":"bool"})'; convertFunction = convertSwapEvent; break; } const event = registry.createType(type, payload)[2].toJSON(); return convertFunction(event); }; const sqrt = (value) => { if (value < 0n) { throw 'square root of negative numbers is not supported'; } if (value < 2n) { return value; } return newtonIteration(value, 1n); }; const newtonIteration = (n, x0) => { const x1 = (n / x0 + x0) >> 1n; if (x0 === x1 || x0 === x1 - 1n) { return x0; } return newtonIteration(n, x1); }; export const calculatePriceImpact = (startingSqrtPrice, endingSqrtPrice) => { const startingPrice = startingSqrtPrice * startingSqrtPrice; const endingPrice = endingSqrtPrice * endingSqrtPrice; const diff = startingPrice - endingPrice; const nominator = diff > 0n ? diff : -diff; const denominator = startingPrice > endingPrice ? startingPrice : endingPrice; return (nominator * getPercentageDenominator()) / denominator; }; export const sqrtPriceToPrice = (sqrtPrice) => { return ((sqrtPrice * sqrtPrice) / getSqrtPriceDenominator()); }; export const priceToSqrtPrice = (price) => { return sqrt(price * getSqrtPriceDenominator()); }; export const calculateLiquidityBreakpoints = (ticks) => { let currentLiquidity = 0n; return ticks.map(tick => { currentLiquidity = currentLiquidity + tick.liquidityChange * (tick.sign ? 1n : -1n); return { liquidity: currentLiquidity, index: tick.index }; }); }; export const calculateSqrtPriceAfterSlippage = (sqrtPrice, slippage, up) => { if (slippage === 0n) { return sqrtPrice; } const percentageDenominator = getPercentageDenominator(); const multiplier = percentageDenominator + (up ? slippage : -slippage); const price = sqrtPriceToPrice(sqrtPrice); const priceWithSlippage = price * multiplier * percentageDenominator; const sqrtPriceWithSlippage = priceToSqrtPrice(priceWithSlippage) / percentageDenominator; return sqrtPriceWithSlippage; }; export function filterTicks(ticks, tickIndex, xToY) { const filteredTicks = new Array(...ticks); let tickCount = 0; for (const [index, tick] of filteredTicks.entries()) { if (tickCount >= MAX_SWAP_STEPS) { break; } if (xToY) { if (tick.index > tickIndex) { filteredTicks.splice(index, 1); } } else { if (tick.index < tickIndex) { filteredTicks.splice(index, 1); } } tickCount++; } return filteredTicks; } export function filterTickmap(tickmap, tickSpacing, index, xToY) { const filteredTickmap = new Map(tickmap.bitmap); const [currentChunkIndex] = tickIndexToPosition(index, tickSpacing); let tickCount = 0; for (const [chunkIndex] of filteredTickmap) { if (tickCount >= MAX_SWAP_STEPS) { break; } if (xToY) { if (chunkIndex > currentChunkIndex) { filteredTickmap.delete(chunkIndex); } } else { if (chunkIndex < currentChunkIndex) { filteredTickmap.delete(chunkIndex); } } tickCount++; } return { bitmap: filteredTickmap }; } export const delay = (delayMs) => { return new Promise(resolve => setTimeout(resolve, delayMs)); }; export const calculateTokenAmounts = (pool, position) => { return _calculateTokenAmounts(pool, position, false); }; export const _calculateTokenAmounts = (pool, position, sign) => { return wasmSerializer.decodeCalculateAmountDeltaResult(_calculateAmountDelta(pool.currentTickIndex, pool.sqrtPrice, wasmSerializer.encodeLiquidity(position.liquidity), sign, position.upperTickIndex, position.lowerTickIndex)); }; export const newFeeTier = (fee, tickSpacing) => { return convertFeeTier(_newFeeTier(fee, tickSpacing)); }; export const newPoolKey = (token0, token1, feeTier) => { return convertPoolKey(_newPoolKey(token0, token1, feeTier)); }; export const calculateFee = (pool, position, lowerTick, upperTick) => { return _calculateFee(lowerTick.index, lowerTick.feeGrowthOutsideX, lowerTick.feeGrowthOutsideY, upperTick.index, upperTick.feeGrowthOutsideX, upperTick.feeGrowthOutsideY, pool.currentTickIndex, pool.feeGrowthGlobalX, pool.feeGrowthGlobalY, position.feeGrowthInsideX, position.feeGrowthInsideY, wasmSerializer.encodeLiquidity(position.liquidity)).map(wasmSerializer.decodeTokenAmount); }; export const getLiquidityByX = (amountX, lowerTick, upperTick, sqrtPrice, roundingUp) => { return wasmSerializer.decodeSingleTokenLiquidity(_getLiquidityByX(wasmSerializer.encodeTokenAmount(amountX), lowerTick, upperTick, sqrtPrice, roundingUp)); }; export const getLiquidityByY = (amountY, lowerTick, upperTick, sqrtPrice, roundingUp) => { return wasmSerializer.decodeSingleTokenLiquidity(_getLiquidityByY(wasmSerializer.encodeTokenAmount(amountY), integerSafeCast(lowerTick), integerSafeCast(upperTick), sqrtPrice, roundingUp)); }; export const calculateTick = (sqrtPrice, tickSpacing) => { return _calculateTick(sqrtPrice, tickSpacing); }; export const isTokenX = (token0, token1) => { return _isTokenX(token0, token1); }; export const getMinSqrtPrice = (tickSpacing) => { return _getMinSqrtPrice(tickSpacing); }; export const getMaxSqrtPrice = (tickSpacing) => { return _getMaxSqrtPrice(tickSpacing); }; export const getMaxChunk = (tickSpacing) => { return BigInt(_getMaxChunk(tickSpacing)); }; export const getMaxTick = (tickSpacing) => { return BigInt(_getMaxTick(tickSpacing)); }; export const getMinTick = (tickSpacing) => { return BigInt(_getMinTick(tickSpacing)); }; export const toFeeGrowth = (val, scale) => { return _toFeeGrowth(val, integerSafeCast(scale)); }; export const toLiquidity = (val, scale) => { return _toLiquidity(val, integerSafeCast(scale)); }; export const toFixedPoint = (val, scale) => { return _toFixedPoint(val, integerSafeCast(scale)); }; export const toPercentage = (val, scale) => { return _toPercentage(val, integerSafeCast(scale)); }; export const toPrice = (val, scale) => { return _toPrice(val, integerSafeCast(scale)); }; export const toSecondsPerLiquidity = (val, scale) => { return _toSecondsPerLiquidity(val, integerSafeCast(scale)); }; export const toSqrtPrice = (val, scale) => { return _toSqrtPrice(val, integerSafeCast(scale)); }; export const toTokenAmount = (val, scale) => { return _toTokenAmount(val, integerSafeCast(scale)); }; export const positionToTick = (chunk, bit, tickSpacing) => { return BigInt(_positionToTick(integerSafeCast(chunk), integerSafeCast(bit), integerSafeCast(tickSpacing))); }; export const calculateSqrtPrice = (tickIndex) => { return _calculateSqrtPrice(tickIndex); }; export const simulateInvariantSwap = (tickmap, feeTier, pool, liquidityTicks, xToY, amount, byAmountIn, sqrtPriceLimit) => { return wasmSerializer.decodeSimulateSwapResult(_simulateInvariantSwap(tickmap, feeTier, wasmSerializer.encodePool(pool), liquidityTicks.map(wasmSerializer.encodeLiquidityTick), xToY, wasmSerializer.encodeTokenAmount(amount), byAmountIn, sqrtPriceLimit)); }; export const calculateFeeTierWithLinearRatio = (tickCount) => { return newFeeTier(tickCount * toPercentage(1n, 4n), tickCount); }; export const calculateConcentration = (tickSpacing, minimumRange, n) => { const concentration = 1 / (1 - Math.pow(1.0001, (-tickSpacing * (minimumRange + 2 * n)) / 4)); return concentration / CONCENTRATION_FACTOR; }; export const calculateTickDelta = (tickSpacing, minimumRange, concentration) => { const base = Math.pow(1.0001, -(tickSpacing / 4)); const logArg = (1 - 1 / (concentration * CONCENTRATION_FACTOR)) / Math.pow(1.0001, (-tickSpacing * minimumRange) / 4); return Math.ceil(Math.log(logArg) / Math.log(base) / 2); }; export const getConcentrationArray = (tickSpacing, minimumRange, currentTick) => { const concentrations = []; let counter = 0; let concentration = 0; let lastConcentration = calculateConcentration(tickSpacing, minimumRange, counter) + 1; let concentrationDelta = 1; while (concentrationDelta >= 1) { concentration = calculateConcentration(tickSpacing, minimumRange, counter); concentrations.push(concentration); concentrationDelta = lastConcentration - concentration; lastConcentration = concentration; counter++; } concentration = Math.ceil(concentrations[concentrations.length - 1]); while (concentration > 1) { concentrations.push(concentration); concentration--; } const maxTick = integerSafeCast(_alignTickToSpacing(getMaxTick(1n), tickSpacing)); if ((minimumRange / 2) * tickSpacing > maxTick - Math.abs(currentTick)) { throw new Error(String(InvariantError.TickLimitReached)); } const limitIndex = (maxTick - Math.abs(currentTick) - (minimumRange / 2) * tickSpacing) / tickSpacing; return concentrations.slice(0, limitIndex); }; export const calculateAmountDelta = (currentTickIndex, currentSqrtPrice, liquidity, roundingUp, upperTickIndex, lowerTickIndex) => { const encodedLiquidity = wasmSerializer.encodeLiquidity(liquidity); const [x, y] = _calculateAmountDelta(currentTickIndex, currentSqrtPrice, encodedLiquidity, roundingUp, upperTickIndex, lowerTickIndex); return [wasmSerializer.decodeTokenAmount(x), wasmSerializer.decodeTokenAmount(y)]; }; export const calculateTokenAmountsWithSlippage = (tickSpacing, currentSqrtPrice, liquidity, lowerTickIndex, upperTickIndex, slippage, roundingUp) => { const lowerBound = calculateSqrtPriceAfterSlippage(currentSqrtPrice, slippage, false); const upperBound = calculateSqrtPriceAfterSlippage(currentSqrtPrice, slippage, true); const currentTickIndex = calculateTick(currentSqrtPrice, tickSpacing); const [lowerX, lowerY] = calculateAmountDelta(currentTickIndex, lowerBound, liquidity, roundingUp, upperTickIndex, lowerTickIndex); const [upperX, upperY] = calculateAmountDelta(currentTickIndex, upperBound, liquidity, roundingUp, upperTickIndex, lowerTickIndex); const x = lowerX > upperX ? lowerX : upperX; const y = lowerY > upperY ? lowerY : upperY; return [x, y]; }; export class BatchError extends Error { failedTxs; constructor(failedTxs) { let message = 'Batch error occurred'; failedTxs.forEach(function (err, nr) { message = message + `\nRequest number ${nr} failed: ${err}`; }); super(message); this.failedTxs = failedTxs; } } export const batchTxs = async (api, account, transactions, options = {}) => { const methods = transactions.map(val => val.extrinsic); const validationCallbacks = transactions.map(val => val.validation); const tx = api.tx.utility.batchAll([...methods]); await tx.signAsync(account, options); const res = await new Promise((resolve, reject) => tx .send(({ events, status }) => { if (status.isInBlock) { const msgData = []; events.forEach(({ event }) => { const { method, section, data } = event; if (method === 'MessageQueued' && section === 'gear') { const { id, destination } = data; msgData.push([id.toHex(), status.asInBlock.toHex(), destination.toHex()]); } else if (method === 'ExtrinsicSuccess') { resolve(msgData); } else if (method === 'ExtrinsicFailed') { reject(api.getExtrinsicFailedError(event)); } }); } }) .catch(error => { reject(error.message); })); const messages = await res; const responsePromises = messages.map(([msgId, blockHash, programId]) => { return new Promise(async (resolve) => { const res = await api.message.getReplyEvent(programId, msgId, blockHash); resolve(res); }); }); const responses = await Promise.all(responsePromises); const errors = new Map(); for (let i = 0; i < responses.length; i++) { const response = responses[i]; const message = response.data.message; const validationCallback = validationCallbacks[i]; if (!message.details.unwrap().code.isSuccess) { errors.set(i, api.registry.createType('String', message.payload).toString()); continue; } if (validationCallback) { const errorFromAdditionalCheck = validationCallback(response); if (errorFromAdditionalCheck) { errors.set(i, errorFromAdditionalCheck); } } } if (errors.size) { throw new BatchError(errors); } return responses; };