UNPKG

@dolomite-exchange/dolomite-margin

Version:

Ethereum Smart Contracts and TypeScript library used for the DolomiteMargin trading protocol

454 lines 21.3 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.AccountOperation = void 0; const bignumber_js_1 = __importDefault(require("bignumber.js")); const BytesHelper_1 = require("../../lib/BytesHelper"); const Constants_1 = require("../../lib/Constants"); const expiry_constants_json_1 = __importDefault(require("../../lib/expiry-constants.json")); const Helpers_1 = require("../../lib/Helpers"); const types_1 = require("../../types"); class AccountOperation { constructor(contracts, orderMapper, networkId, options) { // use the passed-in proxy type, but support the old way of passing in `usePayableProxy = true` const proxy = options.proxy || types_1.ProxyType.None; this.contracts = contracts; this.actions = []; this.committed = false; this.orderMapper = orderMapper; this.accounts = []; this.proxy = proxy; this.sendEthTo = options.sendEthTo; this.auths = []; this.networkId = networkId; } deposit(deposit) { this.addActionArgs(deposit, { actionType: types_1.ActionType.Deposit, amount: deposit.amount, otherAddress: deposit.from, primaryMarketId: deposit.marketId.toFixed(0), }); return this; } withdraw(withdraw) { this.addActionArgs(withdraw, { amount: withdraw.amount, actionType: types_1.ActionType.Withdraw, otherAddress: withdraw.to, primaryMarketId: withdraw.marketId.toFixed(0), }); return this; } transfer(transfer) { this.addActionArgs(transfer, { actionType: types_1.ActionType.Transfer, amount: transfer.amount, primaryMarketId: transfer.marketId.toFixed(0), otherAccountId: this.getAccountId(transfer.toAccountOwner, transfer.toAccountId), }); return this; } buy(buy) { return this.exchange(buy, types_1.ActionType.Buy); } sell(sell) { return this.exchange(sell, types_1.ActionType.Sell); } liquidate(liquidate) { this.addActionArgs(liquidate, { actionType: types_1.ActionType.Liquidate, amount: liquidate.amount, primaryMarketId: liquidate.liquidMarketId.toFixed(0), secondaryMarketId: liquidate.payoutMarketId.toFixed(0), otherAccountId: this.getAccountId(liquidate.liquidAccountOwner, liquidate.liquidAccountId), }); return this; } vaporize(vaporize) { this.addActionArgs(vaporize, { actionType: types_1.ActionType.Vaporize, amount: vaporize.amount, primaryMarketId: vaporize.vaporMarketId.toFixed(0), secondaryMarketId: vaporize.payoutMarketId.toFixed(0), otherAccountId: this.getAccountId(vaporize.vaporAccountOwner, vaporize.vaporAccountId), }); return this; } setApprovalForExpiry(args) { this.addActionArgs(args, { actionType: types_1.ActionType.Call, otherAddress: this.contracts.expiry.options.address, data: BytesHelper_1.toBytes(types_1.ExpiryCallFunctionType.SetApproval, args.sender, args.minTimeDelta), }); return this; } setExpiry(args) { let callData = BytesHelper_1.toBytes(types_1.ExpiryCallFunctionType.SetExpiry); callData = callData.concat(BytesHelper_1.toBytes(new bignumber_js_1.default(64))); callData = callData.concat(BytesHelper_1.toBytes(new bignumber_js_1.default(args.expiryArgs.length))); for (let i = 0; i < args.expiryArgs.length; i += 1) { const expiryArg = args.expiryArgs[i]; callData = callData.concat(BytesHelper_1.toBytes(expiryArg.accountOwner, expiryArg.accountId, expiryArg.marketId, expiryArg.timeDelta, expiryArg.forceUpdate)); } this.addActionArgs(args, { actionType: types_1.ActionType.Call, otherAddress: this.contracts.expiry.options.address, data: callData, }); return this; } call(args) { this.addActionArgs(args, { actionType: types_1.ActionType.Call, otherAddress: args.callee, data: args.data, }); return this; } trade(trade) { let callData = BytesHelper_1.toBytes(trade.calculateAmountWithMakerAccount); callData = callData.concat(BytesHelper_1.toBytes(new bignumber_js_1.default(64))); // offset to where the bytes array is callData = callData.concat(BytesHelper_1.toBytes(trade.data.length)); // length of the array callData = callData.concat(BytesHelper_1.hexStringToBytes(BytesHelper_1.bytesToHexString(trade.data))); this.addActionArgs(trade, { actionType: types_1.ActionType.Trade, amount: trade.amount, primaryMarketId: trade.inputMarketId.toFixed(0), secondaryMarketId: trade.outputMarketId.toFixed(0), otherAccountId: this.getAccountId(trade.otherAccountOwner, trade.otherAccountId), otherAddress: trade.autoTrader, data: callData, }); return this; } liquidateExpiredAccount(liquidate, maxExpiry) { return this.liquidateExpiredAccountInternal(liquidate, maxExpiry || Constants_1.INTEGERS.ONES_31, this.contracts.expiry.options.address); } fullyLiquidateExpiredAccount(primaryAccountOwner, primaryAccountNumber, expiredAccountOwner, expiredAccountNumber, expiredMarket, expiryTimestamp, blockTimestamp, weis, prices, spreadPremiums, collateralPreferences) { return this.fullyLiquidateExpiredAccountInternal(primaryAccountOwner, primaryAccountNumber, expiredAccountOwner, expiredAccountNumber, expiredMarket, expiryTimestamp, blockTimestamp, weis, prices, spreadPremiums, collateralPreferences, this.contracts.expiry.options.address); } /** * Adds all actions from a SignedOperation and also adds the authorization object that allows the * proxy to process the actions. */ addSignedOperation(signedOperation) { // throw error if operation is not going to use the signed proxy if (this.proxy !== types_1.ProxyType.Signed) { throw new Error('Cannot add signed operation if not using signed operation proxy'); } // store the auth this.auths.push({ startIndex: new bignumber_js_1.default(this.actions.length), numActions: new bignumber_js_1.default(signedOperation.actions.length), salt: signedOperation.salt, expiration: signedOperation.expiration, sender: signedOperation.sender, signer: signedOperation.signer, typedSignature: signedOperation.typedSignature, }); // store the actions for (let i = 0; i < signedOperation.actions.length; i += 1) { const action = signedOperation.actions[i]; const secondaryAccountId = action.secondaryAccountOwner === Constants_1.ADDRESSES.ZERO ? 0 : this.getAccountId(action.secondaryAccountOwner, action.secondaryAccountNumber); this.addActionArgs({ primaryAccountOwner: action.primaryAccountOwner, primaryAccountId: action.primaryAccountNumber, }, { actionType: action.actionType, primaryMarketId: action.primaryMarketId.toFixed(0), secondaryMarketId: action.secondaryMarketId.toFixed(0), otherAddress: action.otherAddress, otherAccountId: secondaryAccountId, data: BytesHelper_1.hexStringToBytes(action.data), amount: { reference: action.amount.ref, denomination: action.amount.denomination, value: action.amount.value.times(action.amount.sign ? 1 : -1), }, }); } return this; } /** * Takes all current actions/accounts and creates an Operation struct that can then be signed and * later used with the SignedOperationProxy. */ createSignableOperation(options = {}) { if (this.auths.length) { throw new Error('Cannot create operation out of operation with auths'); } if (!this.actions.length) { throw new Error('Cannot create operation out of operation with no actions'); } const actionArgsToAction = (action) => { const secondaryAccount = action.actionType === types_1.ActionType.Transfer || action.actionType === types_1.ActionType.Trade || action.actionType === types_1.ActionType.Liquidate || action.actionType === types_1.ActionType.Vaporize ? this.accounts[action.otherAccountId] : { owner: Constants_1.ADDRESSES.ZERO, number: '0' }; return { actionType: Helpers_1.toNumber(action.actionType), primaryAccountOwner: this.accounts[action.accountId].owner, primaryAccountNumber: new bignumber_js_1.default(this.accounts[action.accountId].number), secondaryAccountOwner: secondaryAccount.owner, secondaryAccountNumber: new bignumber_js_1.default(secondaryAccount.number), primaryMarketId: new bignumber_js_1.default(action.primaryMarketId), secondaryMarketId: new bignumber_js_1.default(action.secondaryMarketId), amount: { sign: action.amount.sign, ref: Helpers_1.toNumber(action.amount.ref), denomination: Helpers_1.toNumber(action.amount.denomination), value: new bignumber_js_1.default(action.amount.value), }, otherAddress: action.otherAddress, data: BytesHelper_1.bytesToHexString(action.data), }; }; const actions = this.actions.map(actionArgsToAction.bind(this)); return { actions, expiration: options.expiration || Constants_1.INTEGERS.ZERO, salt: options.salt || Constants_1.INTEGERS.ZERO, sender: options.sender || Constants_1.ADDRESSES.ZERO, signer: options.signer || this.accounts[0].owner, }; } /** * Commits the operation to the chain by sending a transaction. */ async commit(options) { if (this.committed) { throw new Error('Operation already committed'); } if (this.actions.length === 0) { throw new Error('No actions have been added to operation'); } if (options && options.confirmationType !== types_1.ConfirmationType.Simulate) { this.committed = true; } try { let method; switch (this.proxy) { case types_1.ProxyType.None: method = this.contracts.dolomiteMargin.methods.operate(this.accounts, this.actions); break; case types_1.ProxyType.Payable: method = this.contracts.payableProxy.methods.operate(this.accounts, this.actions, this.sendEthTo || (options && options.from) || this.contracts.payableProxy.options.from); break; case types_1.ProxyType.Signed: method = this.contracts.signedOperationProxy.methods.operate(this.accounts, this.actions, this.generateAuthData()); break; default: // noinspection ExceptionCaughtLocallyJS throw new Error(`Invalid proxy type: ${this.proxy}`); } return this.contracts.callContractFunction(method, options); } catch (error) { this.committed = false; throw error; } } liquidateExpiredAccountInternal(liquidate, maxExpiryTimestamp, contractAddress) { this.addActionArgs(liquidate, { actionType: types_1.ActionType.Trade, amount: liquidate.amount, primaryMarketId: liquidate.liquidMarketId.toFixed(0), secondaryMarketId: liquidate.payoutMarketId.toFixed(0), otherAccountId: this.getAccountId(liquidate.liquidAccountOwner, liquidate.liquidAccountId), otherAddress: contractAddress, data: BytesHelper_1.toBytes( /* calculateAmountWithMakerAccount */ true, 64, // position offset of dynamic byte array 64, // length of dynamic array in bytes BytesHelper_1.bytesToHexString(BytesHelper_1.toBytes(liquidate.liquidMarketId, maxExpiryTimestamp))), }); return this; } fullyLiquidateExpiredAccountInternal(primaryAccountOwner, primaryAccountNumber, expiredAccountOwner, expiredAccountNumber, expiredMarket, expiryTimestamp, blockTimestamp, weis, prices, spreadPremiums, collateralPreferences, contractAddress) { // hardcoded values const networkExpiryConstants = expiry_constants_json_1.default[this.networkId]; const defaultSpread = new bignumber_js_1.default(networkExpiryConstants.spread); const expiryRampTime = new bignumber_js_1.default(networkExpiryConstants.expiryRampTime); // get info about the expired market let owedWei = weis[expiredMarket.toNumber()]; const owedPrice = prices[expiredMarket.toNumber()]; const owedSpreadMultiplier = spreadPremiums[expiredMarket.toNumber()].plus(1); // error checking if (owedWei.gte(0)) { throw new Error('Expired account must have negative expired balance'); } if (blockTimestamp.lt(expiryTimestamp)) { throw new Error('Expiry timestamp must be larger than blockTimestamp'); } // loop through each collateral type as long as there is some debt left for (let i = 0; i < collateralPreferences.length && owedWei.lt(0); i += 1) { // get info about the next collateral market const heldMarket = collateralPreferences[i]; const heldWei = weis[heldMarket.toFixed(0)]; // skip this collateral market if the account is not positive in this market if (heldWei.lte(0)) { continue; } const heldPrice = prices[heldMarket.toFixed(0)]; const heldSpreadMultiplier = spreadPremiums[heldMarket.toFixed(0)].plus(1); // get the relative value of each market const rampAdjustment = bignumber_js_1.default.min(blockTimestamp.minus(expiryTimestamp).div(expiryRampTime), Constants_1.INTEGERS.ONE); const spread = defaultSpread.times(heldSpreadMultiplier).times(owedSpreadMultiplier).plus(1); const heldValue = heldWei.times(heldPrice).abs(); const owedValue = owedWei.times(owedPrice).times(rampAdjustment).times(spread).abs(); // add variables that need to be populated let primaryMarketId; let secondaryMarketId; // set remaining owedWei and the marketIds depending on which market will 'bound' the action if (heldValue.gt(owedValue)) { // we expect no remaining owedWei owedWei = Constants_1.INTEGERS.ZERO; primaryMarketId = expiredMarket; secondaryMarketId = heldMarket; } else { // calculate the expected remaining owedWei owedWei = owedValue.minus(heldValue).div(owedValue).times(owedWei); primaryMarketId = heldMarket; secondaryMarketId = expiredMarket; } // add the action to the current actions this.addActionArgs({ primaryAccountOwner, primaryAccountId: primaryAccountNumber, }, { actionType: types_1.ActionType.Trade, amount: { value: Constants_1.INTEGERS.ZERO, denomination: types_1.AmountDenomination.Principal, reference: types_1.AmountReference.Target, }, primaryMarketId: primaryMarketId.toFixed(0), secondaryMarketId: secondaryMarketId.toFixed(0), otherAccountId: this.getAccountId(expiredAccountOwner, expiredAccountNumber), otherAddress: contractAddress, data: BytesHelper_1.toBytes( /* calculateAmountWithMakerAccount */ true, 64, // position offset of dynamic byte array 64, // length of dynamic array in bytes BytesHelper_1.bytesToHexString(BytesHelper_1.toBytes(expiredMarket, expiryTimestamp))), }); } return this; } // ============ Private Helper Functions ============ exchange(exchange, actionType) { const { bytes, exchangeWrapperAddress, } = this.orderMapper.mapOrder(exchange.order); const [primaryMarketId, secondaryMarketId] = actionType === types_1.ActionType.Buy ? [exchange.makerMarketId, exchange.takerMarketId] : [exchange.takerMarketId, exchange.makerMarketId]; const orderData = bytes.map((a) => [a]); this.addActionArgs(exchange, { actionType, amount: exchange.amount, otherAddress: exchangeWrapperAddress, data: orderData, primaryMarketId: primaryMarketId.toFixed(0), secondaryMarketId: secondaryMarketId.toFixed(0), }); return this; } addActionArgs(action, args) { if (this.committed) { throw new Error('Operation already committed'); } const amount = args.amount ? { sign: !args.amount.value.isNegative(), denomination: args.amount.denomination, ref: args.amount.reference, value: args.amount.value.abs().toFixed(0), } : { sign: false, denomination: 0, ref: 0, value: 0, }; const actionArgs = { amount, accountId: this.getPrimaryAccountId(action), actionType: args.actionType, primaryMarketId: args.primaryMarketId || '0', secondaryMarketId: args.secondaryMarketId || '0', otherAddress: args.otherAddress || Constants_1.ADDRESSES.ZERO, otherAccountId: args.otherAccountId || '0', data: args.data || [], }; this.actions.push(actionArgs); } getPrimaryAccountId(operation) { return this.getAccountId(operation.primaryAccountOwner, operation.primaryAccountId); } getAccountId(accountOwner, accountNumber) { const accountInfo = { owner: accountOwner, number: accountNumber.toFixed(0), }; const correctIndex = (i) => BytesHelper_1.addressesAreEqual(i.owner, accountInfo.owner) && i.number === accountInfo.number; const index = this.accounts.findIndex(correctIndex); if (index >= 0) { return index; } this.accounts.push(accountInfo); return this.accounts.length - 1; } generateAuthData() { let actionIndex = Constants_1.INTEGERS.ZERO; const result = []; const emptyAuth = { numActions: '0', header: { expiration: '0', salt: '0', sender: Constants_1.ADDRESSES.ZERO, signer: Constants_1.ADDRESSES.ZERO, }, signature: [], }; // for each signed auth for (let i = 0; i < this.auths.length; i += 1) { const auth = this.auths[i]; // if empty auth needed, push it if (auth.startIndex.gt(actionIndex)) { result.push({ ...emptyAuth, numActions: auth.startIndex.minus(actionIndex).toFixed(0), }); } // push this auth result.push({ numActions: auth.numActions.toFixed(0), header: { expiration: auth.expiration.toFixed(0), salt: auth.salt.toFixed(0), sender: auth.sender, signer: auth.signer, }, signature: BytesHelper_1.toBytes(auth.typedSignature), }); // update the action index actionIndex = auth.startIndex.plus(auth.numActions); } // push a final empty auth if necessary if (actionIndex.lt(this.actions.length)) { result.push({ ...emptyAuth, numActions: new bignumber_js_1.default(this.actions.length).minus(actionIndex).toFixed(0), }); } return result; } } exports.AccountOperation = AccountOperation; //# sourceMappingURL=AccountOperation.js.map