@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
JavaScript
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;
};