@broxus/js-core
Version:
MobX-based JavaScript Core library
410 lines (409 loc) • 17.5 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
import { debounce, debug, sliceString, throwException } from '@broxus/js-utils';
import { BigNumber } from 'bignumber.js';
import { action, computed, makeObservable } from 'mobx';
import { inheritTextStyle, successLabelStyle, successTextStyle, warningLabelStyle, warningTextStyle } from '../../console';
import { DEFAULT_NATIVE_CURRENCY_DECIMALS, ZeroAddress } from '../../constants';
import { SmartContractModel } from '../../core';
import { dexPairCallbacksContract, } from '../../models/dex-pair';
import { DexStablePoolUtils, } from '../../models/dex-stable-pool/DexStablePoolUtils';
import { TvmToken } from '../../models/tvm-token';
import { areAddressesEqual, contractStateChangeDebugMessage, getRandomInt, ProviderNotDefinedError, resolveTvmAddress, subscribeDebugMessage, syncErrorMessage, toInt, unsubscribeDebugMessage, unsubscribeErrorMessage, } from '../../utils';
export class DexStablePool extends SmartContractModel {
_connection;
dex;
options;
_provider;
static Utils = DexStablePoolUtils;
/**
* @param {ProviderRpcClient} _connection
* Standalone RPC client that doesn't require connection to the TVM wallet provider
* @param {Address | string} address
* DexStablePool root address
* @param {Dex} dex
* Dex Smart Contract Model instance
* @param {Readonly<DexStablePoolCtorOptions>} [options]
* (optional) DexStablePool Smart Contract Model options
* @param {ProviderRpcClient} [_provider]
* (optional) RPC provider that require connection to the TVM wallet
*/
constructor(_connection, address, dex, options, _provider) {
super(_connection, address);
this._connection = _connection;
this.dex = dex;
this.options = options;
this._provider = _provider;
this.setData(() => ({
feeParams: {},
}));
makeObservable(this);
}
/**
* @param {ProviderRpcClient} connection
* Standalone RPC client that doesn't require connection to the TVM wallet provider
* @param {Readonly<DexStablePoolCreateConfig>} config
* DexStablePool Smart Contract Model config
* @param {Readonly<DexStablePoolCreateOptions>} [options]
* (optional) DexStablePool Smart Contract Model options
* @param {ProviderRpcClient} [provider]
* (optional) RPC provider that require connection to the TVM wallet
*/
static async create(connection, config, options, provider) {
const { sync = true, watch, watchCallback, ...restOptions } = { ...options };
let address;
if ('address' in config) {
address = config.address;
}
else {
address = await config.dex.getExpectedPoolAddress({
roots: config.roots,
});
}
const dexStablePool = new DexStablePool(connection, address, config.dex, restOptions, provider);
if (sync) {
await dexStablePool.sync({ force: false });
}
if (watch) {
await dexStablePool.watch(watchCallback);
}
return dexStablePool;
}
async sync(options) {
if (!options?.force && this.isSyncing) {
return;
}
try {
this.setState('isSyncing', !options?.silent);
const state = await this.syncContractState({ force: options?.force || !this.contractState });
if (!this.isDeployed) {
throwException('DexStablePool is not deployed');
}
const [details, feeParams, dexRootAddress, dexVaultAddress, virtualPrice] = await Promise.all([
DexStablePool.Utils.getDetails(this._connection, this.address, state),
DexStablePool.Utils.getFeeParams(this._connection, this.address, state),
DexStablePool.Utils.getRoot(this._connection, this.address, state),
DexStablePool.Utils.getVault(this._connection, this.address, state),
DexStablePool.Utils.getVirtualPrice(this._connection, this.address, state),
]);
this.setData({ ...details, dexRootAddress, dexVaultAddress, feeParams, virtualPrice });
}
catch (e) {
if (process.env.NODE_ENV !== 'production') {
const state = await this._connection.getProviderState();
syncErrorMessage(this.constructor.name, this.address, e, state.networkId.toString());
}
}
finally {
this.setState('isSyncing', false);
}
}
async watch(callback) {
try {
this.contractSubscriber = new this._connection.Subscriber();
await this.contractSubscriber.states(this.address).delayed(stream => {
if (process.env.NODE_ENV !== 'production') {
subscribeDebugMessage(this.constructor.name, this.address);
}
return stream.on(debounce(async (event) => {
if (process.env.NODE_ENV !== 'production') {
const state = await this._connection.getProviderState();
contractStateChangeDebugMessage(this.constructor.name, this.address, event, state.networkId.toString());
}
if (areAddressesEqual(event.address, this.address)) {
await this.sync({ force: !this.isSyncing, silent: true });
callback?.(...this.toJSON(true));
return;
}
await this.unwatch();
}, this.options?.watchDebounceDelay ?? 3000));
});
return this.contractSubscriber;
}
catch (e) {
await this.unwatch();
throw e;
}
}
async unwatch() {
try {
await this.contractSubscriber?.unsubscribe();
this.contractSubscriber = undefined;
if (process.env.NODE_ENV !== 'production') {
unsubscribeDebugMessage(this.constructor.name, this.address);
}
}
catch (e) {
if (process.env.NODE_ENV !== 'production') {
const state = await this._connection.getProviderState();
unsubscribeErrorMessage(this.constructor.name, this.address, e, state.networkId.toString());
}
}
}
async check(options) {
if (options?.force || !this.contractState) {
await this.syncContractState({ force: options?.force ?? true });
}
if (!this?.isDeployed) {
debug(`%c${this.constructor.name}%c Check for %c${sliceString(this.address?.toString())}%c =>%c not deployed`, warningLabelStyle, inheritTextStyle, successTextStyle, inheritTextStyle, warningTextStyle);
return false;
}
const isActive = await DexStablePool.Utils.isActive(this._connection, this.address, this.contractState);
if (!isActive) {
debug(`%c${this.constructor.name}%c Check for %c${sliceString(this.address?.toString())}%c =>%c inactive`, warningLabelStyle, inheritTextStyle, successTextStyle, inheritTextStyle, warningTextStyle);
return false;
}
debug(`%c${this.constructor.name}%c Check for %c${sliceString(this.address?.toString())}%c =>%c deployed`, successLabelStyle, inheritTextStyle, successTextStyle, inheritTextStyle, successTextStyle);
return true;
}
async syncBalances(options) {
if (!this.lpToken || !this.tokens.length) {
await this.sync({ force: true });
return DexStablePool.Utils.getBalances(this._connection, this.address, this.contractState);
}
await this.syncContractState({ force: options?.force ?? true });
const balances = await DexStablePool.Utils.getBalances(this._connection, this.address, this.contractState);
this.setData('balances', balances);
return balances;
}
async deployPool(params, args) {
if (await this.check({ force: true })) {
throw new Error('DexStablePool already deployed');
}
return this.dex.deployStablePool({
...params,
expectedAddress: this.address,
roots: this.tokens.map(token => token.root),
}, args);
}
async withdrawLiquidity(params, args) {
if (!this._provider) {
throw new ProviderNotDefinedError(this.constructor.name);
}
const callId = params.callId ?? getRandomInt();
const subscriber = new this._connection.Subscriber();
let transaction;
try {
await this.sync({ force: true });
let { ownerLpWalletAddress, payload } = params;
if (!ownerLpWalletAddress) {
ownerLpWalletAddress = await TvmToken.Utils.walletOf(this._connection, {
ownerAddress: params.ownerAddress,
tokenAddress: this.lpToken.address,
}, this.lpToken.contractState);
}
const lpUserWallet = await TvmToken.Wallet.create(this._connection, { address: ownerLpWalletAddress }, { sync: true }, this._provider);
if (!this.lpWallet?.isDeployed || !lpUserWallet?.isDeployed) {
throwException('LP wallets do not exist');
}
if (!payload) {
const castValue = (value) => BigNumber(value)
.times(0.999)
.dp(0, BigNumber.ROUND_DOWN)
.toFixed();
const expected = await DexStablePool.Utils.expectedWithdrawLiquidity(this._connection, this.address, params.amount, this.contractState);
payload = await DexStablePool.Utils.buildWithdrawLiquidityPayload(this._connection, this.address, {
callId,
deployWalletGrams: params.deployWalletGrams ?? toInt(0.1, DEFAULT_NATIVE_CURRENCY_DECIMALS),
expectedAmounts: expected.amounts.map(castValue),
recipient: params.ownerAddress,
referrer: params.referrer ?? ZeroAddress,
}, this.contractState);
}
const message = await lpUserWallet.transferToWallet({
amount: params.amount,
notify: params.notify ?? true,
payload,
recipientTokenWallet: this.lpWallet.address,
remainingGasTo: params.remainingGasTo ?? params.ownerAddress,
}, {
amount: toInt(2.7, DEFAULT_NATIVE_CURRENCY_DECIMALS), // => 2.7 EVER as minimum value
bounce: true,
from: resolveTvmAddress(params.ownerAddress),
...args,
});
await params.onSend?.(message, { callId });
transaction = await message.transaction;
await params.onTransactionSent?.({ callId, transaction });
const stream = await subscriber
.trace(transaction)
.filterMap(async (tx) => {
if (!areAddressesEqual(tx.account, params.ownerAddress)) {
return undefined;
}
const dexPairCallbacks = dexPairCallbacksContract(this._connection, params.ownerAddress);
const decodedTx = await dexPairCallbacks.decodeTransaction({
methods: ['dexPairOperationCancelled', 'dexPairWithdrawSuccessV2'],
transaction: tx,
});
if (decodedTx?.method === 'dexPairWithdrawSuccessV2') {
const result = {
callId,
input: decodedTx.input,
transaction: tx,
};
debug('dexPairWithdrawSuccessV2', result);
await params.onTransactionSuccess?.(result);
return result;
}
if (decodedTx?.method === 'dexPairOperationCancelled') {
const reason = {
callId,
input: decodedTx.input,
transaction: tx,
};
debug('dexPairOperationCancelled', reason);
await params.onTransactionFailure?.(reason);
return reason;
}
return undefined;
})
.delayed(s => s.first());
await stream();
return transaction;
}
catch (e) {
params.onTransactionFailure?.({
callId,
error: e,
transaction,
});
throw e;
}
finally {
if (process.env.NODE_ENV !== 'production') {
debug(`%c${this.constructor.name}%c Unsubscribed from the withdrawal liquidity stream`, warningLabelStyle, inheritTextStyle);
}
await subscriber.unsubscribe();
}
}
get balances() {
return this._data.balances;
}
get dexRootAddress() {
return this._data.dexRootAddress;
}
get dexVaultAddress() {
return this._data.dexVaultAddress;
}
get feeParams() {
return this._data.feeParams;
}
get isActive() {
return this._data.isActive;
}
get lpToken() {
return this._data.lpToken;
}
get lpWallet() {
return this._data.lpWallet;
}
get tokens() {
return this._data.tokens;
}
get virtualPrice() {
return this._data.virtualPrice;
}
get wallets() {
return this._data.wallets;
}
}
__decorate([
action.bound,
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], DexStablePool.prototype, "sync", null);
__decorate([
action.bound,
__metadata("design:type", Function),
__metadata("design:paramtypes", [Function]),
__metadata("design:returntype", Promise)
], DexStablePool.prototype, "watch", null);
__decorate([
action.bound,
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], DexStablePool.prototype, "unwatch", null);
__decorate([
action.bound,
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], DexStablePool.prototype, "check", null);
__decorate([
action.bound,
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], DexStablePool.prototype, "syncBalances", null);
__decorate([
action.bound,
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, Object]),
__metadata("design:returntype", Promise)
], DexStablePool.prototype, "deployPool", null);
__decorate([
action.bound,
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, Object]),
__metadata("design:returntype", Promise)
], DexStablePool.prototype, "withdrawLiquidity", null);
__decorate([
computed,
__metadata("design:type", Object),
__metadata("design:paramtypes", [])
], DexStablePool.prototype, "balances", null);
__decorate([
computed,
__metadata("design:type", Object),
__metadata("design:paramtypes", [])
], DexStablePool.prototype, "dexRootAddress", null);
__decorate([
computed,
__metadata("design:type", Object),
__metadata("design:paramtypes", [])
], DexStablePool.prototype, "dexVaultAddress", null);
__decorate([
computed,
__metadata("design:type", Object),
__metadata("design:paramtypes", [])
], DexStablePool.prototype, "feeParams", null);
__decorate([
computed,
__metadata("design:type", Object),
__metadata("design:paramtypes", [])
], DexStablePool.prototype, "isActive", null);
__decorate([
computed,
__metadata("design:type", Object),
__metadata("design:paramtypes", [])
], DexStablePool.prototype, "lpToken", null);
__decorate([
computed,
__metadata("design:type", Object),
__metadata("design:paramtypes", [])
], DexStablePool.prototype, "lpWallet", null);
__decorate([
computed,
__metadata("design:type", Object),
__metadata("design:paramtypes", [])
], DexStablePool.prototype, "tokens", null);
__decorate([
computed,
__metadata("design:type", Object),
__metadata("design:paramtypes", [])
], DexStablePool.prototype, "virtualPrice", null);
__decorate([
computed,
__metadata("design:type", Object),
__metadata("design:paramtypes", [])
], DexStablePool.prototype, "wallets", null);