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