UNPKG

@f5i23q999d/cow-sdk

Version:

<p align="center"> <img width="400" src="https://github.com/cowprotocol/cow-sdk/raw/main/docs/images/CoW.png" /> </p>

398 lines 18.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.transformStructToData = exports.transformDataToStruct = exports.Twap = exports.StartTimeValue = exports.DurationType = exports.MAX_FREQUENCY = exports.MAX_UINT32 = exports.CURRENT_BLOCK_TIMESTAMP_FACTORY_ADDRESS = exports.TWAP_ADDRESS = void 0; const ethers_1 = require("ethers"); const ConditionalOrder_1 = require("../ConditionalOrder.js"); const types_1 = require("../types.js"); const utils_1 = require("../utils.js"); // The type of Conditional Order const TWAP_ORDER_TYPE = 'twap'; // The address of the TWAP handler contract exports.TWAP_ADDRESS = '0x6cF1e9cA41f7611dEf408122793c358a3d11E5a5'; /** * The address of the `CurrentBlockTimestampFactory` contract * * **NOTE**: This is used in the event that TWAP's have a `t0` of `0`. */ exports.CURRENT_BLOCK_TIMESTAMP_FACTORY_ADDRESS = '0x52eD56Da04309Aca4c3FECC595298d80C2f16BAc'; exports.MAX_UINT32 = ethers_1.BigNumber.from(2).pow(32).sub(1); // 2^32 - 1 exports.MAX_FREQUENCY = ethers_1.BigNumber.from(365 * 24 * 60 * 60); // 1 year // Define the ABI tuple for the TWAPData struct const TWAP_STRUCT_ABI = [ 'tuple(address sellToken, address buyToken, address receiver, uint256 partSellAmount, uint256 minPartLimit, uint256 t0, uint256 n, uint256 t, uint256 span, bytes32 appData)', ]; var DurationType; (function (DurationType) { DurationType["AUTO"] = "AUTO"; DurationType["LIMIT_DURATION"] = "LIMIT_DURATION"; })(DurationType = exports.DurationType || (exports.DurationType = {})); var StartTimeValue; (function (StartTimeValue) { StartTimeValue["AT_MINING_TIME"] = "AT_MINING_TIME"; StartTimeValue["AT_EPOCH"] = "AT_EPOCH"; })(StartTimeValue = exports.StartTimeValue || (exports.StartTimeValue = {})); const DEFAULT_START_TIME = { startType: StartTimeValue.AT_MINING_TIME }; const DEFAULT_DURATION_OF_PART = { durationType: DurationType.AUTO }; /** * `ComposableCoW` implementation of a TWAP order. * @author mfw78 <mfw78@rndlabs.xyz> */ class Twap extends ConditionalOrder_1.ConditionalOrder { isSingleOrder = true; /** * @see {@link ConditionalOrder.constructor} * @throws If the TWAP order is invalid. * @throws If the TWAP order is not ABI-encodable. * @throws If the handler is not the TWAP address. */ constructor(params) { const { handler, salt, data: staticInput, hasOffChainInput } = params; // First, verify that the handler is the TWAP address if (handler !== exports.TWAP_ADDRESS) throw new Error(`InvalidHandler: Expected: ${exports.TWAP_ADDRESS}, provided: ${handler}`); // Third, construct the base class using transformed parameters super({ handler: exports.TWAP_ADDRESS, salt, data: staticInput, hasOffChainInput }); } /** * Create a TWAP order with sound defaults. * @param data The TWAP order parameters in a more user-friendly format. * @returns An instance of the TWAP order. */ static fromData(data, salt) { return new Twap({ handler: exports.TWAP_ADDRESS, data, salt }); } /** * Create a TWAP order with sound defaults. * @param data The TWAP order parameters in a more user-friendly format. * @returns An instance of the TWAP order. */ static fromParams(params) { return Twap.deserialize((0, utils_1.encodeParams)(params)); } /** * Enforces that TWAPs will commence at the beginning of a block by use of the * `CurrentBlockTimestampFactory` contract to provide the current block timestamp * as the start time of the TWAP. */ get context() { if (this.staticInput.t0.gt(0)) { return super.context; } else { return { address: exports.CURRENT_BLOCK_TIMESTAMP_FACTORY_ADDRESS, factoryArgs: undefined, }; } } /** * @inheritdoc {@link ConditionalOrder.orderType} */ get orderType() { return TWAP_ORDER_TYPE; } /** * Validate the TWAP order. * @param data The TWAP order to validate. * @returns Whether the TWAP order is valid. * @throws If the TWAP order is invalid. * @see {@link TwapStruct} for the native struct. */ isValid() { const error = (() => { const { sellToken, sellAmount, buyToken, buyAmount, startTime = DEFAULT_START_TIME, numberOfParts, timeBetweenParts, durationOfPart = DEFAULT_DURATION_OF_PART, } = this.data; // Verify that the order params are logically valid if (!(sellToken != buyToken)) return 'InvalidSameToken'; if (!(sellToken != ethers_1.constants.AddressZero && buyToken != ethers_1.constants.AddressZero)) return 'InvalidToken'; if (!sellAmount.gt(ethers_1.constants.Zero)) return 'InvalidSellAmount'; if (!buyAmount.gt(ethers_1.constants.Zero)) return 'InvalidMinBuyAmount'; if (startTime.startType === StartTimeValue.AT_EPOCH) { const t0 = startTime.epoch; if (!(t0.gte(ethers_1.constants.Zero) && t0.lt(exports.MAX_UINT32))) return 'InvalidStartTime'; } if (!(numberOfParts.gt(ethers_1.constants.One) && numberOfParts.lte(exports.MAX_UINT32))) return 'InvalidNumParts'; if (!(timeBetweenParts.gt(ethers_1.constants.Zero) && timeBetweenParts.lte(exports.MAX_FREQUENCY))) return 'InvalidFrequency'; if (durationOfPart.durationType === DurationType.LIMIT_DURATION) { if (!durationOfPart.duration.lte(timeBetweenParts)) return 'InvalidSpan'; } // Verify that the staticInput derived from the data is ABI-encodable if (!(0, utils_1.isValidAbi)(TWAP_STRUCT_ABI, [this.staticInput])) return 'InvalidData'; // No errors return undefined; })(); return error ? { isValid: false, reason: error } : { isValid: true }; } async startTimestamp(params) { const { startTime } = this.data; if (startTime?.startType === StartTimeValue.AT_EPOCH) { return startTime.epoch.toNumber(); } const cabinet = await this.cabinet(params); const rawCabinetEpoch = ethers_1.utils.defaultAbiCoder.decode(['uint256'], cabinet)[0]; // Guard against out-of-range cabinet epoch if (rawCabinetEpoch.gt(exports.MAX_UINT32)) { throw new Error(`Cabinet epoch out of range: ${rawCabinetEpoch.toString()}`); } // Convert the cabinet epoch (bignumber) to a number. const cabinetEpoch = rawCabinetEpoch.toNumber(); if (cabinetEpoch === 0) { throw new Error('Cabinet is not set. Required for TWAP orders that start at mining time.'); } return cabinetEpoch; } /** * Given the start timestamp of the TWAP, calculate the end timestamp. * @dev As usually the `endTimestamp` is used when determining a TWAP's validity, we don't * do any lookup to the blockchain to determine the start timestamp, as this has likely * already been done during the verification flow. * @dev Beware to handle the case of `span != 0` ie. `durationOfPart.durationType !== DurationType.AUTO`. * @param startTimestamp The start timestamp of the TWAP. * @returns The timestamp at which the TWAP will end. */ endTimestamp(startTimestamp) { const { numberOfParts, timeBetweenParts, durationOfPart } = this.data; if (durationOfPart && durationOfPart.durationType === DurationType.LIMIT_DURATION) { return startTimestamp + numberOfParts.sub(1).mul(timeBetweenParts).add(durationOfPart.duration).toNumber(); } return startTimestamp + numberOfParts.mul(timeBetweenParts).toNumber(); } /** * Checks if the owner authorized the conditional order. * * @param owner The owner of the conditional order. * @param chain Which chain to use for the ComposableCoW contract. * @param provider An RPC provider for the chain. * @returns true if the owner authorized the order, false otherwise. */ async pollValidate(params) { const { blockInfo = await (0, utils_1.getBlockInfo)(params.provider) } = params; const { blockTimestamp } = blockInfo; try { const startTimestamp = await this.startTimestamp(params); if (startTimestamp > blockTimestamp) { // The start time hasn't started return { result: types_1.PollResultCode.TRY_AT_EPOCH, epoch: startTimestamp, reason: `TWAP hasn't started yet. Starts at ${startTimestamp} (${(0, utils_1.formatEpoch)(startTimestamp)})`, }; } const expirationTimestamp = this.endTimestamp(startTimestamp); if (blockTimestamp >= expirationTimestamp) { // The order has expired return { result: types_1.PollResultCode.DONT_TRY_AGAIN, reason: `TWAP has expired. Expired at ${expirationTimestamp} (${(0, utils_1.formatEpoch)(expirationTimestamp)})`, }; } return undefined; } catch (err) { if (err?.message?.includes('Cabinet is not set')) { // in this case we have a firm reason to not monitor this order as the cabinet is not set return { result: types_1.PollResultCode.DONT_TRY_AGAIN, reason: `${err?.message}. User likely removed the order.`, }; } else if (err?.message?.includes('Cabinet epoch out of range')) { // in this case we have a firm reason to not monitor this order as the cabinet is not set correctly return { result: types_1.PollResultCode.DONT_TRY_AGAIN, reason: `${err?.message}`, }; } return { result: types_1.PollResultCode.UNEXPECTED_ERROR, reason: `Unexpected error: ${err.message}`, error: err, }; } } /** * Handles the error when the order is already present in the orderbook. * * Given the current part is in the book, it will signal to Watch Tower what to do: * - Wait until the next part starts * - Don't try again if current part is the last one * * NOTE: The error messages will refer to the parts 1-indexed, so first part is 1, second part is 2, etc. */ async handlePollFailedAlreadyPresent(_orderUid, _order, params) { const { blockInfo = await (0, utils_1.getBlockInfo)(params.provider) } = params; const { blockTimestamp } = blockInfo; const timeBetweenParts = this.data.timeBetweenParts.toNumber(); const { numberOfParts } = this.data; const startTimestamp = await this.startTimestamp(params); if (blockTimestamp < startTimestamp) { return { result: types_1.PollResultCode.UNEXPECTED_ERROR, reason: `TWAP part hash't started. First TWAP part start at ${startTimestamp} (${(0, utils_1.formatEpoch)(startTimestamp)})`, error: undefined, }; } const expireTime = numberOfParts.mul(timeBetweenParts).add(startTimestamp).toNumber(); if (blockTimestamp >= expireTime) { return { result: types_1.PollResultCode.UNEXPECTED_ERROR, reason: `TWAP is expired. Expired at ${expireTime} (${(0, utils_1.formatEpoch)(expireTime)})`, error: undefined, }; } // Get current part number const currentPartNumber = Math.floor((blockTimestamp - startTimestamp) / timeBetweenParts); // If current part is the last one if (currentPartNumber === numberOfParts.toNumber() - 1) { return { result: types_1.PollResultCode.DONT_TRY_AGAIN, reason: `Current active TWAP part (${currentPartNumber + 1}/${numberOfParts}) is already in the Order Book. This was the last TWAP part, no more orders need to be placed`, }; } // Next part start time const nextPartStartTime = startTimestamp + (currentPartNumber + 1) * timeBetweenParts; /** * Given we know, that TWAP part that is due in the current block is already in the Orderbook, * Then, we can safely instruct that we should wait until the next TWAP part starts */ return { result: types_1.PollResultCode.TRY_AT_EPOCH, epoch: nextPartStartTime, reason: `Current active TWAP part (${currentPartNumber + 1}/${numberOfParts}) is already in the Order Book. TWAP part ${currentPartNumber + 2} doesn't start until ${nextPartStartTime} (${(0, utils_1.formatEpoch)(nextPartStartTime)})`, }; } /** * Serialize the TWAP order into it's ABI-encoded form. * @returns {string} The ABI-encoded TWAP order. */ serialize() { return (0, utils_1.encodeParams)(this.leaf); } /** * Get the encoded static input for the TWAP order. * @returns {string} The ABI-encoded TWAP order. */ encodeStaticInput() { return super.encodeStaticInputHelper(TWAP_STRUCT_ABI, this.staticInput); } /** * Deserialize a TWAP order from it's ABI-encoded form. * @param {string} twapSerialized ABI-encoded TWAP order to deserialize. * @returns A deserialized TWAP order. */ static deserialize(twapSerialized) { return super.deserializeHelper(twapSerialized, exports.TWAP_ADDRESS, TWAP_STRUCT_ABI, (struct, salt) => new Twap({ handler: exports.TWAP_ADDRESS, salt, data: transformStructToData(struct), })); } /** * Create a human-readable string representation of the TWAP order. * @returns {string} A human-readable string representation of the TWAP order. */ toString() { const { sellAmount, sellToken, buyAmount, buyToken, numberOfParts, startTime = DEFAULT_START_TIME, timeBetweenParts, durationOfPart = DEFAULT_DURATION_OF_PART, receiver, appData, } = this.data; const startTimeFormatted = startTime.startType === StartTimeValue.AT_MINING_TIME ? 'AT_MINING_TIME' : startTime.epoch.toNumber(); const durationOfPartFormatted = durationOfPart.durationType === DurationType.AUTO ? 'AUTO' : durationOfPart.duration.toNumber(); const details = { sellAmount: sellAmount.toString(), sellToken, buyAmount: buyAmount.toString(), buyToken, numberOfParts: numberOfParts.toString(), startTime: startTimeFormatted, timeBetweenParts: timeBetweenParts.toNumber(), durationOfPart: durationOfPartFormatted, receiver, appData, }; return `${this.orderType} (${this.id}): ${JSON.stringify(details)}`; } /** * Transform parameters into a native struct. * * @param {TwapData} data As passed by the consumer of the API. * @returns {TwapStruct} A formatted struct as expected by the smart contract. */ transformDataToStruct(data) { return transformDataToStruct(data); } /** * Transform parameters into a TWAP order struct. * * @param {TwapData} params As passed by the consumer of the API. * @returns {TwapStruct} A formatted struct as expected by the smart contract. */ transformStructToData(struct) { return transformStructToData(struct); } } exports.Twap = Twap; /** * Transform parameters into a native struct. * * @param {TwapData} data As passed by the consumer of the API. * @returns {TwapStruct} A formatted struct as expected by the smart contract. */ function transformDataToStruct(data) { const { sellAmount, buyAmount, numberOfParts, startTime: startTime = DEFAULT_START_TIME, timeBetweenParts, durationOfPart = DEFAULT_DURATION_OF_PART, ...rest } = data; const { partSellAmount, minPartLimit } = numberOfParts && !numberOfParts.isZero() ? { partSellAmount: sellAmount.div(numberOfParts), minPartLimit: buyAmount.div(numberOfParts), } : { partSellAmount: ethers_1.constants.Zero, minPartLimit: ethers_1.constants.Zero, }; const span = durationOfPart.durationType === DurationType.AUTO ? ethers_1.constants.Zero : durationOfPart.duration; const t0 = startTime.startType === StartTimeValue.AT_MINING_TIME ? ethers_1.constants.Zero : startTime.epoch; return { partSellAmount, minPartLimit, t0, n: numberOfParts, t: timeBetweenParts, span, ...rest, }; } exports.transformDataToStruct = transformDataToStruct; /** * Transform parameters into a TWAP order struct. * * @param {TwapData} params As passed by the consumer of the API. * @returns {TwapStruct} A formatted struct as expected by the smart contract. */ function transformStructToData(struct) { const { n: numberOfParts, partSellAmount, minPartLimit, t: timeBetweenParts, t0: startEpoch, span, sellToken, buyToken, receiver, appData, } = struct; const durationOfPart = span.isZero() ? { durationType: DurationType.AUTO } : { durationType: DurationType.LIMIT_DURATION, duration: span }; const startTime = span.isZero() ? { startType: StartTimeValue.AT_MINING_TIME } : { startType: StartTimeValue.AT_EPOCH, epoch: startEpoch }; return { sellAmount: partSellAmount.mul(numberOfParts), buyAmount: minPartLimit.mul(numberOfParts), startTime, numberOfParts, timeBetweenParts, durationOfPart, sellToken, buyToken, receiver, appData, }; } exports.transformStructToData = transformStructToData; //# sourceMappingURL=Twap.js.map