@broxus/js-core
Version:
MobX-based JavaScript Core library
808 lines (807 loc) • 33.9 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, error, sliceString, throwException } from '@broxus/js-utils';
import { action, computed, makeObservable } from 'mobx';
import { errorTextStyle, inheritTextStyle, successLabelStyle, successTextStyle, warningLabelStyle } from '../../console';
import { DEFAULT_NATIVE_CURRENCY_DECIMALS } from '../../constants';
import { SmartContractModel } from '../../core';
import { DexAccountUtils } from '../../models/dex-account/DexAccountUtils';
import { TvmToken } from '../../models/tvm-token';
import { TvmTokenWallet } from '../../models/tvm-token-wallet';
import { walletContract } from '../../models/wallet';
import { areAddressesEqual, contractStateChangeDebugMessage, getRandomInt, ProviderNotDefinedError, resolveTvmAddress, subscribeDebugMessage, syncErrorMessage, toInt, unsubscribeDebugMessage, unsubscribeErrorMessage, } from '../../utils';
export class DexAccount extends SmartContractModel {
_connection;
dex;
options;
_provider;
static Utils = DexAccountUtils;
/**
* @param {ProviderRpcClient} _connection
* Standalone RPC client that doesn't require connection to the TVM wallet provider
* @param {Address | string} address
* DexAccount root address
* @param {Dex} dex Dex Smart Contract Model instance
* @param {Readonly<DexAccountCtorOptions>} [options]
* (optional) DexAccount 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;
makeObservable(this);
}
/**
* @param {ProviderRpcClient} connection
* Standalone RPC client that doesn't require connection to the TVM wallet provider
* @param {Readonly<DexAccountCreateConfig>} config
* DexAccount Smart Contract Model config
* @param {Readonly<DexAccountCtorOptions>} [options]
* (optional) DexAccount 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.getExpectedAccountAddress({
ownerAddress: config.ownerAddress,
});
}
const dexAccount = new DexAccount(connection, address, config.dex, restOptions, provider);
if (sync) {
await dexAccount.sync({ force: false });
}
if (watch) {
await dexAccount.watch(watchCallback);
}
return dexAccount;
}
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 });
await this.syncComputedStorageData();
if (!this.isDeployed) {
throwException('DexAccount is not deployed');
}
const [balances, ownerAddress, wallets] = await Promise.all([
DexAccount.Utils.getBalances(this._connection, this.address, state),
DexAccount.Utils.getOwner(this._connection, this.address, state),
DexAccount.Utils.getWallets(this._connection, this.address, state),
]);
this.setData({ balances, ownerAddress, wallets });
}
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 account %c${sliceString(this.address?.toString())}%c =>%c not deployed`, warningLabelStyle, inheritTextStyle, successTextStyle, inheritTextStyle, errorTextStyle);
return false;
}
try {
await DexAccount.Utils.getVersion(this._connection, this.address, this.contractState);
return true;
}
catch (e) {
return false;
}
}
/**
* Stream-based method to add pair (constant or stable) to the DexAccount
* @param {DexAccountDepositTokenParams} params
* @param {Partial<SendInternalParams>} [args]
* @deprecated Use `DexAccount.addPool` instead
*/
async addPair(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 {
const message = await DexAccount.Utils.addPair(this._provider, this.address, {
leftRootAddress: params.leftRootAddress,
rightRootAddress: params.rightRootAddress,
}, 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, this.address)) {
return undefined;
}
try {
const events = await this.decodeTransactionEvents(tx);
if (events.length === 0) {
return undefined;
}
const event = events.find(e => e.event === 'AddPool');
if (!event) {
return undefined;
}
if (process.env.NODE_ENV !== 'production') {
debug(`%c${this.constructor.name}%c %cAddPool%c event was captured`, successLabelStyle, inheritTextStyle, successTextStyle, inheritTextStyle, event);
}
await this.sync({ force: true });
const [leftRootAddress, rightRootAddress] = event.data.roots;
await params.onTransactionSuccess?.({
callId,
input: {
leftRootAddress,
pairAddress: event.data.pair,
rightRootAddress,
},
transaction: tx,
});
return event;
}
catch (e) {
error('AddPool event captured with an error', e);
await params.onTransactionFailure?.({
callId,
error: e,
transaction: tx,
});
return null;
}
})
.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 adding pair stream`, warningLabelStyle, inheritTextStyle);
}
await subscriber?.unsubscribe();
}
}
/**
* Stream-based method to add pool (stable) to the DexAccount
* @param {DexAccountDepositTokenParams} params
* @param {Partial<SendInternalParams>} [args]
*/
async addPool(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 {
const message = await DexAccount.Utils.addPool(this._provider, this.address, {
roots: params.roots,
}, 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, this.address)) {
return undefined;
}
try {
const events = await this.decodeTransactionEvents(tx);
if (events.length === 0) {
return undefined;
}
const event = events.find(e => e.event === 'AddPool');
if (!event) {
return undefined;
}
if (process.env.NODE_ENV !== 'production') {
debug(`%c${this.constructor.name}%c %cAddPool%c event was captured`, successLabelStyle, inheritTextStyle, successTextStyle, inheritTextStyle, event);
}
await this.sync({ force: true });
await params.onTransactionSuccess?.({
callId,
input: {
poolAddress: event.data.pair,
roots: event.data.roots,
},
transaction: tx,
});
return event;
}
catch (e) {
error('AddPool event captured with an error', e);
await params.onTransactionFailure?.({
callId,
error: e,
transaction: tx,
});
return null;
}
})
.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 adding stable pool stream`, warningLabelStyle, inheritTextStyle);
}
await subscriber?.unsubscribe();
}
}
/**
* Stream-based method to deposit a given token to the DexAccount
* @param {DexAccountDepositTokenParams} params
* @param {Partial<SendInternalParams>} [args]
*/
async depositToken(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 {
const senderTokenWalletAddress = params.senderTokenWalletAddress
?? (params.senderAddress && params.tokenAddress
? await TvmToken.Utils.walletOf(this._connection, {
ownerAddress: params.senderAddress,
tokenAddress: params.tokenAddress,
})
: undefined);
if (!senderTokenWalletAddress) {
throwException('Sender token wallet address is not specified.');
}
const message = await TvmTokenWallet.Utils.transferToWallet(this._provider, senderTokenWalletAddress, {
amount: params.amount,
recipientTokenWallet: params.recipientTokenWallet,
remainingGasTo: params.remainingGasTo,
}, {
amount: toInt(1.5, DEFAULT_NATIVE_CURRENCY_DECIMALS),
...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, this.address)) {
return undefined;
}
try {
const events = await this.decodeTransactionEvents(tx);
if (events.length === 0) {
return undefined;
}
const event = events.find(e => e.event === 'TokensReceived');
if (!event) {
return undefined;
}
if (process.env.NODE_ENV !== 'production') {
debug(`%c${this.constructor.name}%c %cTokensReceived%c event was captured`, successLabelStyle, inheritTextStyle, successTextStyle, inheritTextStyle, event);
}
await params.onTransactionSuccess?.({
callId,
input: {
amount: event.data.tokens_amount,
balance: event.data.balance,
tokenAddress: event.data.token_root,
},
transaction: tx,
});
return event;
}
catch (e) {
error('TokensReceived event captured with an error', e);
await params.onTransactionFailure?.({
callId,
error: e,
transaction: tx,
});
return null;
}
})
.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 depositing token [%c${sliceString(params.tokenAddress?.toString())}%c] stream`, warningLabelStyle, inheritTextStyle, successTextStyle, inheritTextStyle);
}
await subscriber?.unsubscribe();
}
}
/**
* Stream-based method to deposit a liquidity from the DexAccount assets
* @param {DexAccountDepositLiquidityParams} params
* @param {Partial<SendInternalParams>} [args]
*/
async depositLiquidity(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 {
const senderAddress = resolveTvmAddress(params.senderAddress);
const message = await DexAccount.Utils.depositLiquidity(this._provider, this.address, {
autoChange: params.autoChange,
callId,
expectedLpAddress: params.expectedLpAddress,
leftAmount: params.leftAmount,
leftRootAddress: params.leftRootAddress,
rightAmount: params.rightAmount,
rightRootAddress: params.rightRootAddress,
sendGasTo: params.sendGasTo ?? params.senderAddress,
}, 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, senderAddress)) {
return undefined;
}
try {
const wallet = walletContract(this._connection, senderAddress);
const decodedTx = await wallet.decodeTransaction({
methods: [
'dexPairOperationCancelled',
'dexPairDepositLiquiditySuccess',
'dexPairDepositLiquiditySuccessV2',
],
transaction: tx,
});
if (decodedTx?.method === 'dexPairOperationCancelled') {
const reason = {
callId,
input: decodedTx.input,
transaction: tx,
};
debug('dexPairOperationCancelled', reason);
await params.onTransactionFailure?.(reason);
return reason;
}
if (decodedTx?.method === 'dexPairDepositLiquiditySuccessV2') {
const result = {
callId,
input: { ...decodedTx.input, type: 'stable' },
transaction: tx,
};
debug('dexPairDepositLiquiditySuccessV2', result);
await params.onTransactionSuccess?.(result);
return result;
}
if (decodedTx?.method === 'dexPairDepositLiquiditySuccess') {
const result = {
callId,
input: { ...decodedTx.input, type: 'constant' },
transaction: tx,
};
debug('dexPairDepositLiquiditySuccess', result);
await params.onTransactionSuccess?.(result);
return result;
}
return undefined;
}
catch (e) {
error('Deposit liquidity failed with an error', e);
params.onTransactionFailure?.({
callId,
error: e,
transaction: tx,
});
return null;
}
})
.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 depositing liquidity stream`, warningLabelStyle, inheritTextStyle, successTextStyle, inheritTextStyle);
}
await subscriber?.unsubscribe();
}
}
async depositLiquidityV2(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 {
const senderAddress = resolveTvmAddress(params.senderAddress);
const message = await DexAccount.Utils.depositLiquidityV2(this._provider, this.address, {
callId,
expected: params.expected,
operations: params.operations,
referrer: params.referrer,
remainingGasTo: params.remainingGasTo,
}, 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, senderAddress)) {
return undefined;
}
try {
const wallet = walletContract(this._connection, senderAddress);
const decodedTx = await wallet.decodeTransaction({
methods: ['dexPairOperationCancelled', 'dexPairDepositLiquiditySuccessV2'],
transaction: tx,
});
if (decodedTx?.method === 'dexPairOperationCancelled') {
const reason = {
callId,
input: decodedTx.input,
transaction: tx,
};
debug('dexPairOperationCancelled', reason);
await params.onTransactionFailure?.(reason);
return reason;
}
if (decodedTx?.method === 'dexPairDepositLiquiditySuccessV2') {
const result = {
callId,
input: { ...decodedTx.input, type: 'stable' },
transaction: tx,
};
debug('dexPairDepositLiquiditySuccessV2', result);
await params.onTransactionSuccess?.(result);
return result;
}
return undefined;
}
catch (e) {
error('Deposit liquidity (v2) failed with an error', e);
params.onTransactionFailure?.({
callId,
error: e,
transaction: tx,
});
return null;
}
})
.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 depositing liquidity stream`, warningLabelStyle, inheritTextStyle, successTextStyle, inheritTextStyle);
}
await subscriber?.unsubscribe();
}
}
/**
Stream-based method to withdraw a given token from the DexAccount
* @param {DexAccountWithdrawTokenParams} params
* @param {Partial<SendInternalParams>} [args]
*/
async withdrawToken(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 {
const message = await DexAccount.Utils.withdraw(this._provider, this.address, {
amount: params.amount,
callId,
deployWalletGrams: params.deployWalletGrams,
recipientAddress: params.recipientAddress,
sendGasTo: params.sendGasTo,
tokenAddress: params.tokenAddress,
}, 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, this.address)) {
return undefined;
}
try {
const events = await this.decodeTransactionEvents(tx);
if (events.length === 0) {
return undefined;
}
const event = events.find(e => e.event === 'WithdrawTokens');
if (!event) {
return undefined;
}
if (areAddressesEqual(event.data.root, params.tokenAddress)) {
if (process.env.NODE_ENV !== 'production') {
debug(`%c${this.constructor.name}%c %cWithdrawTokens%c event was captured`, successLabelStyle, inheritTextStyle, successTextStyle, inheritTextStyle, event);
}
await params.onTransactionSuccess?.({
callId,
input: {
amount: event.data.amount,
balance: event.data.balance,
tokenAddress: event.data.root,
},
transaction: tx,
});
return event;
}
return undefined;
}
catch (e) {
error('WithdrawTokens event failed with an error', e);
params.onTransactionFailure?.({
callId,
error: e,
transaction: tx,
});
return null;
}
})
.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 Unsubscribe from the withdrawal token [%c${sliceString(params.tokenAddress?.toString())}%c] stream`, warningLabelStyle, inheritTextStyle, successTextStyle, inheritTextStyle);
}
await subscriber?.unsubscribe();
}
}
/**
* Returns user lp wallets balances as map where
* key is lp wallet address and value is balance.
* @returns {DexAccountData["balances"]}
*/
get balances() {
return this._data.balances;
}
/**
* Returns account owner address
* @returns {DexAccountData["ownerAddress"]}
*/
get ownerAddress() {
return this._data.ownerAddress;
}
/**
* Returns user token wallets addresses as map where
* key is lp token address and value is token wallet address.
* @returns {DexAccountData["wallets"]}
*/
get wallets() {
return this._data.wallets;
}
decodeEvent(args) {
return DexAccount.Utils.decodeEvent(this._connection, this.address, args);
}
decodeTransaction(args) {
return DexAccount.Utils.decodeTransaction(this._connection, this.address, args);
}
decodeTransactionEvents(transaction) {
return DexAccount.Utils.decodeTransactionEvents(this._connection, this.address, transaction);
}
}
__decorate([
action.bound,
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], DexAccount.prototype, "sync", null);
__decorate([
action.bound,
__metadata("design:type", Function),
__metadata("design:paramtypes", [Function]),
__metadata("design:returntype", Promise)
], DexAccount.prototype, "watch", null);
__decorate([
action.bound,
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], DexAccount.prototype, "unwatch", null);
__decorate([
action.bound,
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], DexAccount.prototype, "check", null);
__decorate([
action.bound,
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, Object]),
__metadata("design:returntype", Promise)
], DexAccount.prototype, "addPair", null);
__decorate([
action.bound,
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, Object]),
__metadata("design:returntype", Promise)
], DexAccount.prototype, "addPool", null);
__decorate([
action.bound,
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, Object]),
__metadata("design:returntype", Promise)
], DexAccount.prototype, "depositToken", null);
__decorate([
action.bound,
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, Object]),
__metadata("design:returntype", Promise)
], DexAccount.prototype, "depositLiquidity", null);
__decorate([
action.bound,
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, Object]),
__metadata("design:returntype", Promise)
], DexAccount.prototype, "depositLiquidityV2", null);
__decorate([
action.bound,
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, Object]),
__metadata("design:returntype", Promise)
], DexAccount.prototype, "withdrawToken", null);
__decorate([
computed,
__metadata("design:type", Object),
__metadata("design:paramtypes", [])
], DexAccount.prototype, "balances", null);
__decorate([
computed,
__metadata("design:type", Object),
__metadata("design:paramtypes", [])
], DexAccount.prototype, "ownerAddress", null);
__decorate([
computed,
__metadata("design:type", Object),
__metadata("design:paramtypes", [])
], DexAccount.prototype, "wallets", null);
__decorate([
action.bound,
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], DexAccount.prototype, "decodeEvent", null);
__decorate([
action.bound,
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], DexAccount.prototype, "decodeTransaction", null);
__decorate([
action.bound,
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], DexAccount.prototype, "decodeTransactionEvents", null);