diff --git a/src/composable/ConditionalOrder.spec.ts b/src/composable/ConditionalOrder.spec.ts index e2464191..02fb8db4 100644 --- a/src/composable/ConditionalOrder.spec.ts +++ b/src/composable/ConditionalOrder.spec.ts @@ -25,35 +25,60 @@ const TWAP_SERIALIZED = (salt?: string, handler?: string): string => { ) } -describe('ConditionalOrder', () => { - test('Create: constructor fails if invalid params', () => { +describe('Constuctor', () => { + test('Create TestConditionalOrder', () => { // bad address expect(() => new TestConditionalOrder('0xdeadbeef')).toThrow('Invalid handler: 0xdeadbeef') - // bad salt + }) - expect(() => new TestConditionalOrder('0x910d00a310f7Dc5B29FE73458F47f519be547D3d', 'cowtomoon')).toThrow( - 'Invalid salt: cowtomoon' - ) - expect(() => new TestConditionalOrder('0x910d00a310f7Dc5B29FE73458F47f519be547D3d', '0xdeadbeef')).toThrow( - 'Invalid salt: 0xdeadbeef' - ) - expect( - () => - new TestConditionalOrder( - '0x910d00a310f7Dc5B29FE73458F47f519be547D3d', - '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef' - ) - ).toThrow( - 'Invalid salt: 0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef' + test('Fail if bad address', () => { + // bad address + expect(() => new TestConditionalOrder('0xdeadbeef')).toThrow('Invalid handler: 0xdeadbeef') + }) + + describe('Fail if bad salt', () => { + test('Fails if salt is not an hex', () => { + expect(() => new TestConditionalOrder('0x910d00a310f7Dc5B29FE73458F47f519be547D3d', 'cowtomoon')).toThrow( + 'Invalid salt: cowtomoon' + ) + }) + + test('Fails if salt is too short (not 32 bytes)', () => { + expect(() => new TestConditionalOrder('0x910d00a310f7Dc5B29FE73458F47f519be547D3d', '0xdeadbeef')).toThrow( + 'Invalid salt: 0xdeadbeef' + ) + }) + + test('Fails if salt is too long (not 32 bytes)', () => { + expect( + () => + new TestConditionalOrder( + '0x910d00a310f7Dc5B29FE73458F47f519be547D3d', + '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef' + ) + ).toThrow( + 'Invalid salt: 0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef' + ) + }) + }) +}) +describe('Deserialize: Decode static input', () => { + test('Fails if handler mismatch', () => { + expect(() => Twap.deserialize(TWAP_SERIALIZED(undefined, '0x9008D19f58AAbD9eD0D60971565AA8510560ab41'))).toThrow( + 'HandlerMismatch' ) }) +}) +describe('Serialize: Encode static input', () => { test('Serialize: Fails if invalid params', () => { const order = new TestConditionalOrder('0x910d00a310f7Dc5B29FE73458F47f519be547D3d') expect(() => order.testEncodeStaticInput()).toThrow() }) +}) - test('id: Returns correct id', () => { +describe('Compute orderUid', () => { + test('Returns correct id', () => { const order = new TestConditionalOrder( '0x910d00a310f7Dc5B29FE73458F47f519be547D3d', '0x9379a0bf532ff9a66ffde940f94b1a025d6f18803054c1aef52dc94b15255bbe' @@ -61,7 +86,7 @@ describe('ConditionalOrder', () => { expect(order.id).toEqual('0x88ca0698d8c5500b31015d84fa0166272e1812320d9af8b60e29ae00153363b3') }) - test('leafToId: Returns correct id', () => { + test('Derive OrderId from leaf data', () => { const order = new TestConditionalOrder( '0x910d00a310f7Dc5B29FE73458F47f519be547D3d', '0x9379a0bf532ff9a66ffde940f94b1a025d6f18803054c1aef52dc94b15255bbe' @@ -70,12 +95,6 @@ describe('ConditionalOrder', () => { '0x88ca0698d8c5500b31015d84fa0166272e1812320d9af8b60e29ae00153363b3' ) }) - - test('Deserialize: Fails if handler mismatch', () => { - expect(() => Twap.deserialize(TWAP_SERIALIZED(undefined, '0x9008D19f58AAbD9eD0D60971565AA8510560ab41'))).toThrow( - 'HandlerMismatch' - ) - }) }) class TestConditionalOrder extends ConditionalOrder { diff --git a/src/composable/ConditionalOrder.ts b/src/composable/ConditionalOrder.ts index d347ccec..961e2982 100644 --- a/src/composable/ConditionalOrder.ts +++ b/src/composable/ConditionalOrder.ts @@ -70,6 +70,7 @@ export abstract class ConditionalOrder { this.hasOffChainInput = hasOffChainInput } + // TODO: https://github.com/cowprotocol/cow-sdk/issues/155 abstract get isSingleOrder(): boolean /** @@ -155,6 +156,15 @@ export abstract class ConditionalOrder { return utils.keccak256(this.serialize()) } + /** + * The context key of the order (bytes32(0) if a merkle tree is used, otherwise H(params)) with which to lookup the cabinet + * + * The context, relates to the 'ctx' in the contract: https://github.com/cowprotocol/composable-cow/blob/c7fb85ab10c05e28a1632ba97a1749fb261fcdfb/src/interfaces/IConditionalOrder.sol#L38 + */ + protected get ctx(): string { + return this.isSingleOrder ? this.id : constants.HashZero + } + /** * Get the `leaf` of the conditional order. This is the data that is used to create the merkle tree. * @@ -304,10 +314,8 @@ export abstract class ConditionalOrder { public cabinet(params: OwnerContext): Promise { const { chainId, owner, provider } = params - const slotId = this.isSingleOrder ? this.id : constants.HashZero - const composableCow = getComposableCow(chainId, provider) - return composableCow.callStatic.cabinet(owner, slotId) + return composableCow.callStatic.cabinet(owner, this.ctx) } /** diff --git a/src/composable/orderTypes/Twap.spec.ts b/src/composable/orderTypes/Twap.spec.ts index 56a3ce7b..4ac7f247 100644 --- a/src/composable/orderTypes/Twap.spec.ts +++ b/src/composable/orderTypes/Twap.spec.ts @@ -59,47 +59,81 @@ export function generateRandomTWAPData(): TwapData { } } -describe('Twap', () => { - test('Create: constructor creates valid TWAP', () => { - const twap = Twap.fromData(TWAP_PARAMS_TEST) +describe('Constructor', () => { + test('Create new valid TWAP', () => { + const twap = new Twap({ handler: TWAP_ADDRESS, data: TWAP_PARAMS_TEST }) expect(twap.orderType).toEqual('twap') expect(twap.hasOffChainInput).toEqual(false) expect(twap.offChainInput).toEqual('0x') expect(twap.context?.address).not.toBeUndefined() + }) - const twap2 = Twap.fromData({ ...TWAP_PARAMS_TEST, t0: BigNumber.from(1) }) - expect(twap2.context).toBeUndefined() - + test('Create Twap with invalid handler', () => { expect(() => new Twap({ handler: '0xdeaddeaddeaddeaddeaddeaddeaddeaddeaddead', data: TWAP_PARAMS_TEST })).toThrow( 'InvalidHandler' ) }) +}) - test('isValid: valid twap', () => { +describe('Twap.fromData', () => { + test('Creates valid TWAP: Start at mining time', () => { + const twap = Twap.fromData(TWAP_PARAMS_TEST) + expect(twap.orderType).toEqual('twap') + expect(twap.hasOffChainInput).toEqual(false) + expect(twap.offChainInput).toEqual('0x') + expect(twap.context?.address).not.toBeUndefined() + }) + + test('Creates valid TWAP: Start at epoch', () => { + const twap = Twap.fromData({ + ...TWAP_PARAMS_TEST, + startTime: { startType: StartTimeValue.AT_EPOC, epoch: BigNumber.from(1) }, + }) + expect(twap.context).toBeUndefined() + }) +}) + +describe('Validate', () => { + test('Valid twap', () => { expect(Twap.fromData({ ...TWAP_PARAMS_TEST }).isValid()).toEqual({ isValid: true }) }) - test('isValid: invalid twap', () => { + test('Invalid twap: InvalidSameToken', () => { expect(Twap.fromData({ ...TWAP_PARAMS_TEST, sellToken: TWAP_PARAMS_TEST.buyToken }).isValid()).toEqual({ isValid: false, reason: 'InvalidSameToken', }) + }) + + test('Invalid twap: InvalidToken (sell)', () => { expect(Twap.fromData({ ...TWAP_PARAMS_TEST, sellToken: constants.AddressZero }).isValid()).toEqual({ isValid: false, reason: 'InvalidToken', }) + }) + + test('Invalid twap: InvalidToken (buy)', () => { expect(Twap.fromData({ ...TWAP_PARAMS_TEST, buyToken: constants.AddressZero }).isValid()).toEqual({ isValid: false, reason: 'InvalidToken', }) + }) + + test('Invalid twap: InvalidSellAmount', () => { expect(Twap.fromData({ ...TWAP_PARAMS_TEST, sellAmount: BigNumber.from(0) }).isValid()).toEqual({ isValid: false, reason: 'InvalidSellAmount', }) + }) + + test('Invalid twap: InvalidMinBuyAmount', () => { expect(Twap.fromData({ ...TWAP_PARAMS_TEST, buyAmount: BigNumber.from(0) }).isValid()).toEqual({ isValid: false, reason: 'InvalidMinBuyAmount', }) + }) + + test('Invalid twap: InvalidStartTime', () => { expect( Twap.fromData({ ...TWAP_PARAMS_TEST, @@ -109,14 +143,23 @@ describe('Twap', () => { isValid: false, reason: 'InvalidStartTime', }) + }) + + test('Invalid twap: InvalidNumParts', () => { expect(Twap.fromData({ ...TWAP_PARAMS_TEST, numberOfParts: BigNumber.from(0) }).isValid()).toEqual({ isValid: false, reason: 'InvalidNumParts', }) + }) + + test('Invalid twap: InvalidFrequency', () => { expect(Twap.fromData({ ...TWAP_PARAMS_TEST, timeBetweenParts: BigNumber.from(0) }).isValid()).toEqual({ isValid: false, reason: 'InvalidFrequency', }) + }) + + test('Invalid twap: InvalidSpan (limit duration)', () => { expect( Twap.fromData({ ...TWAP_PARAMS_TEST, @@ -129,13 +172,9 @@ describe('Twap', () => { isValid: false, reason: 'InvalidSpan', }) - expect(Twap.fromData({ ...TWAP_PARAMS_TEST, appData: constants.AddressZero }).isValid()).toEqual({ - isValid: false, - reason: 'InvalidData', - }) }) - test('isValid: Fails if appData has a wrong number of bytes', () => { + test('Invalid twap: InvalidData (ABI parse error in appData)', () => { // The isValid below test triggers a throw by trying to ABI parse `appData` as a `bytes32` when // it only has 20 bytes (ie. an address) expect(Twap.fromData({ ...TWAP_PARAMS_TEST, appData: constants.AddressZero }).isValid()).toEqual({ @@ -143,21 +182,27 @@ describe('Twap', () => { reason: 'InvalidData', }) }) +}) +describe('Serialize', () => { test('serialize: Serializes correctly', () => { const twap = Twap.fromData(TWAP_PARAMS_TEST) expect(twap.serialize()).toEqual(TWAP_SERIALIZED(twap.salt)) }) +}) - test('deserialize: Deserializes correctly', () => { +describe('Deserialize', () => { + test('Deserializes correctly', () => { const twap = Twap.fromData(TWAP_PARAMS_TEST) expect(Twap.deserialize(TWAP_SERIALIZED(twap.salt))).toMatchObject(twap) }) - test('deserialize: Throws if invalid', () => { + test('Throws if invalid', () => { expect(() => Twap.deserialize('0x')).toThrow('InvalidSerializedConditionalOrder') }) +}) +describe('To String', () => { test('toString: Formats correctly', () => { expect(Twap.fromData(TWAP_PARAMS_TEST).toString()).toEqual( 'twap: Sell total 0x6810e776880C02933D47DB1b9fc05908e5386b96@1000000000000000000 for a minimum of 0xDAE5F1590db13E3B40423B5b5c5fbf175515910b@1000000000000000000 over 10 parts with a spacing of 3600s beginning at time of mining' diff --git a/src/composable/orderTypes/Twap.ts b/src/composable/orderTypes/Twap.ts index dece8231..81f52add 100644 --- a/src/composable/orderTypes/Twap.ts +++ b/src/composable/orderTypes/Twap.ts @@ -5,9 +5,8 @@ import { ConditionalOrderArguments, ConditionalOrderParams, ContextFactory, - IsNotValid, - IsValid, OwnerContext, + IsValidResult, PollParams, PollResultCode, PollResultErrors, @@ -229,7 +228,7 @@ export class Twap extends ConditionalOrder { * @throws If the TWAP order is invalid. * @see {@link TwapStruct} for the native struct. */ - isValid(): IsValid | IsNotValid { + isValid(): IsValidResult { const error = (() => { const { sellToken,