74dff2b412
Co-authored-by: Phil Liao <phil@0x.org>
3272 lines
153 KiB
TypeScript
3272 lines
153 KiB
TypeScript
import { TooManyRequestsError } from '@0x/api-utils';
|
|
import { getContractAddressesForChainOrThrow } from '@0x/contract-addresses';
|
|
import {
|
|
eip712SignHashWithKey,
|
|
ethSignHashWithKey,
|
|
MetaTransaction,
|
|
OtcOrder,
|
|
SignatureType,
|
|
} from '@0x/protocol-utils';
|
|
import { BigNumber } from '@0x/utils';
|
|
import { expect } from 'chai';
|
|
import { constants } from 'ethersv5';
|
|
import { Producer } from 'sqs-producer';
|
|
import { anything, capture, deepEqual, instance, mock, spy, verify, when } from 'ts-mockito';
|
|
|
|
import { Integrator } from '../../src/config';
|
|
import {
|
|
DEFAULT_MIN_EXPIRY_DURATION_MS,
|
|
ETH_DECIMALS,
|
|
ONE_MINUTE_MS,
|
|
ONE_SECOND_MS,
|
|
ZERO,
|
|
} from '../../src/core/constants';
|
|
import { RfqmV2JobEntity, RfqmV2QuoteEntity, RfqmV2TransactionSubmissionEntity } from '../../src/entities';
|
|
import {
|
|
RfqmJobStatus,
|
|
RfqmOrderTypes,
|
|
RfqmTransactionSubmissionStatus,
|
|
RfqmTransactionSubmissionType,
|
|
} from '../../src/entities/types';
|
|
import { FeeService } from '../../src/services/fee_service';
|
|
import { RfqmService } from '../../src/services/rfqm_service';
|
|
import { RfqMakerBalanceCacheService } from '../../src/services/rfq_maker_balance_cache_service';
|
|
import {
|
|
ApprovalResponse,
|
|
OtcOrderSubmitRfqmSignedQuoteParams,
|
|
SubmitRfqmSignedQuoteWithApprovalParams,
|
|
} from '../../src/services/types';
|
|
import {
|
|
Approval,
|
|
ExecuteMetaTransactionApproval,
|
|
ExecuteMetaTransactionEip712Context,
|
|
FeeModelVersion,
|
|
GaslessApprovalTypes,
|
|
GaslessTypes,
|
|
IndicativeQuote,
|
|
PermitApproval,
|
|
PermitEip712Context,
|
|
} from '../../src/core/types';
|
|
import { CacheClient } from '../../src/utils/cache_client';
|
|
import { QuoteServerClient } from '../../src/utils/quote_server_client';
|
|
import { otcOrderToStoredOtcOrder, RfqmDbUtils } from '../../src/utils/rfqm_db_utils';
|
|
import { HealthCheckStatus } from '../../src/utils/rfqm_health_check';
|
|
import { RfqBlockchainUtils } from '../../src/utils/rfq_blockchain_utils';
|
|
import { RfqMakerManager } from '../../src/utils/rfq_maker_manager';
|
|
import { TokenMetadataManager } from '../../src/utils/TokenMetadataManager';
|
|
import {
|
|
MOCK_EXECUTE_META_TRANSACTION_APPROVAL,
|
|
MOCK_EXECUTE_META_TRANSACTION_HASH,
|
|
WORKER_TEST_ADDRESS,
|
|
} from '../constants';
|
|
import * as SignatureUtils from '../../src/utils/signature_utils';
|
|
|
|
// $eslint-fix-me https://github.com/rhinodavid/eslint-fix-me
|
|
// eslint-disable-next-line @typescript-eslint/no-loss-of-precision
|
|
const NEVER_EXPIRES = new BigNumber(9999999999999999);
|
|
const MOCK_WORKER_REGISTRY_ADDRESS = '0x1023331a469c6391730ff1E2749422CE8873EC38';
|
|
const MOCK_TOKEN = '0xc2132d05d31c914a87c6611c10748aeb04b58e8f';
|
|
const MOCK_GAS_PRICE = new BigNumber(100000000000);
|
|
const MOCK_MM_URI = 'https://mm-address';
|
|
const WORKER_FULL_BALANCE_WEI = new BigNumber(1).shiftedBy(ETH_DECIMALS);
|
|
const MOCK_INTEGRATOR: Integrator = {
|
|
apiKeys: ['an-integrator-id'],
|
|
integratorId: 'an-integrator-id',
|
|
allowedChainIds: [1337],
|
|
label: 'Test',
|
|
rfqm: true,
|
|
gaslessRfqtVip: true,
|
|
};
|
|
|
|
jest.setTimeout(ONE_MINUTE_MS);
|
|
const buildRfqmServiceForUnitTest = (
|
|
overrides: {
|
|
chainId?: number;
|
|
feeService?: FeeService;
|
|
feeModelVersion?: FeeModelVersion;
|
|
rfqBlockchainUtils?: RfqBlockchainUtils;
|
|
dbUtils?: RfqmDbUtils;
|
|
producer?: Producer;
|
|
quoteServerClient?: QuoteServerClient;
|
|
cacheClient?: CacheClient;
|
|
rfqMakerBalanceCacheService?: RfqMakerBalanceCacheService;
|
|
rfqMakerManager?: RfqMakerManager;
|
|
tokenMetadataManager?: TokenMetadataManager;
|
|
gaslessRfqtVipRolloutPercentage?: number;
|
|
} = {},
|
|
): RfqmService => {
|
|
const contractAddresses = getContractAddressesForChainOrThrow(1);
|
|
const feeServiceMock = mock(FeeService);
|
|
when(feeServiceMock.getGasPriceEstimationAsync()).thenResolve(MOCK_GAS_PRICE);
|
|
when(feeServiceMock.calculateFeeAsync(anything(), anything())).thenResolve({
|
|
feeWithDetails: {
|
|
token: '0xToken',
|
|
amount: new BigNumber(300),
|
|
type: 'fixed',
|
|
details: {
|
|
feeModelVersion: 1,
|
|
kind: 'default',
|
|
gasFeeAmount: new BigNumber(100),
|
|
gasPrice: MOCK_GAS_PRICE,
|
|
zeroExFeeAmount: new BigNumber(200),
|
|
tradeSizeBps: 4,
|
|
feeTokenBaseUnitPriceUsd: new BigNumber(30),
|
|
takerTokenBaseUnitPriceUsd: null,
|
|
makerTokenBaseUnitPriceUsd: new BigNumber(20),
|
|
},
|
|
breakdown: {
|
|
gas: {
|
|
amount: new BigNumber(100),
|
|
details: {
|
|
gasPrice: MOCK_GAS_PRICE,
|
|
estimatedGas: new BigNumber(1),
|
|
},
|
|
},
|
|
zeroEx: {
|
|
amount: new BigNumber(200),
|
|
details: {
|
|
kind: 'volume',
|
|
tradeSizeBps: 4,
|
|
},
|
|
},
|
|
},
|
|
conversionRates: {
|
|
nativeTokenBaseUnitPriceUsd: new BigNumber(30),
|
|
feeTokenBaseUnitPriceUsd: new BigNumber(30),
|
|
takerTokenBaseUnitPriceUsd: null,
|
|
makerTokenBaseUnitPriceUsd: new BigNumber(20),
|
|
},
|
|
},
|
|
});
|
|
const feeServiceInstance = instance(feeServiceMock);
|
|
|
|
const rfqBlockchainUtilsMock = new RfqBlockchainUtils(
|
|
instance(mock()),
|
|
'0xdef1c0ded9bec7f1a1670819833240f027b25eff',
|
|
instance(mock()),
|
|
instance(mock()),
|
|
instance(mock()),
|
|
);
|
|
const spiedRfqBlockchainUtilsMock = spy(rfqBlockchainUtilsMock);
|
|
when(spiedRfqBlockchainUtilsMock.getAccountBalanceAsync(anything())).thenResolve(WORKER_FULL_BALANCE_WEI);
|
|
when(spiedRfqBlockchainUtilsMock.getAllowanceAsync(anything(), anything(), anything())).thenResolve(
|
|
new BigNumber(constants.MaxUint256.toString()),
|
|
new BigNumber(0),
|
|
new BigNumber(0),
|
|
);
|
|
when(spiedRfqBlockchainUtilsMock.getMetaTransactionNonceAsync(anything(), anything())).thenResolve(
|
|
new BigNumber(1),
|
|
);
|
|
const dbUtilsMock = mock(RfqmDbUtils);
|
|
const sqsMock = mock(Producer);
|
|
when(sqsMock.queueSize()).thenResolve(0);
|
|
const quoteServerClientMock = mock(QuoteServerClient);
|
|
const cacheClientMock = mock(CacheClient);
|
|
when(cacheClientMock.getMakersInCooldownForPairAsync(anything(), anything(), anything())).thenResolve([]);
|
|
const rfqMakerBalanceCacheService = mock(RfqMakerBalanceCacheService);
|
|
const rfqMakerManagerMock = mock(RfqMakerManager);
|
|
when(rfqMakerManagerMock.getRfqtV2MakerUrisForPair(anything(), anything(), anything())).thenReturn([MOCK_MM_URI]);
|
|
|
|
const tokenMetadataManagerMock = mock(TokenMetadataManager);
|
|
when(tokenMetadataManagerMock.getTokenDecimalsAsync(anything())).thenResolve(18);
|
|
const tokenMetadataManager = instance(tokenMetadataManagerMock);
|
|
|
|
return new RfqmService(
|
|
overrides.chainId || 1337,
|
|
overrides.feeService || feeServiceInstance,
|
|
overrides.feeModelVersion || 0,
|
|
contractAddresses,
|
|
MOCK_WORKER_REGISTRY_ADDRESS,
|
|
overrides.rfqBlockchainUtils || rfqBlockchainUtilsMock,
|
|
overrides.dbUtils || dbUtilsMock,
|
|
overrides.producer || sqsMock,
|
|
overrides.quoteServerClient || quoteServerClientMock,
|
|
DEFAULT_MIN_EXPIRY_DURATION_MS,
|
|
overrides.cacheClient || instance(cacheClientMock),
|
|
overrides.rfqMakerBalanceCacheService || rfqMakerBalanceCacheService,
|
|
overrides.rfqMakerManager || instance(rfqMakerManagerMock),
|
|
overrides.tokenMetadataManager || tokenMetadataManager,
|
|
overrides.gaslessRfqtVipRolloutPercentage || 0,
|
|
);
|
|
};
|
|
|
|
describe('RfqmService HTTP Logic', () => {
|
|
describe('submitTakerSignedOtcOrderAsync', () => {
|
|
it('should fail if there is already a pending trade for the taker and taker token', async () => {
|
|
const expiry = new BigNumber(Date.now() + 1_000_000).dividedBy(ONE_SECOND_MS).decimalPlaces(0);
|
|
const otcOrder = new OtcOrder({
|
|
txOrigin: '0x0000000000000000000000000000000000000000',
|
|
taker: '0x1111111111111111111111111111111111111111',
|
|
maker: '0x2222222222222222222222222222222222222222',
|
|
makerToken: '0x3333333333333333333333333333333333333333',
|
|
takerToken: '0x4444444444444444444444444444444444444444',
|
|
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(expiry, ZERO, expiry),
|
|
chainId: 1337,
|
|
verifyingContract: '0x0000000000000000000000000000000000000000',
|
|
});
|
|
const existingJob = new RfqmV2JobEntity({
|
|
chainId: 1337,
|
|
expiry,
|
|
makerUri: '',
|
|
orderHash: '0x00',
|
|
fee: {
|
|
token: '0xToken',
|
|
amount: '100',
|
|
type: 'fixed',
|
|
},
|
|
order: otcOrderToStoredOtcOrder(otcOrder),
|
|
workflow: 'rfqm',
|
|
});
|
|
|
|
const quote = new RfqmV2QuoteEntity({
|
|
affiliateAddress: '',
|
|
chainId: 1,
|
|
createdAt: new Date(),
|
|
fee: {
|
|
amount: '0',
|
|
token: '',
|
|
type: 'fixed',
|
|
},
|
|
integratorId: '',
|
|
makerUri: 'http://foo.bar',
|
|
order: {
|
|
order: existingJob.order.order,
|
|
type: RfqmOrderTypes.Otc,
|
|
},
|
|
orderHash: '',
|
|
isUnwrap: false,
|
|
workflow: 'rfqm',
|
|
});
|
|
|
|
const dbUtilsMock = mock(RfqmDbUtils);
|
|
when(dbUtilsMock.findV2JobsWithStatusesAsync(anything())).thenResolve([existingJob]);
|
|
when(dbUtilsMock.findV2QuoteByOrderHashAsync(otcOrder.getHash())).thenResolve(quote);
|
|
const params: OtcOrderSubmitRfqmSignedQuoteParams = {
|
|
type: GaslessTypes.OtcOrder,
|
|
order: otcOrder,
|
|
signature: {
|
|
r: '',
|
|
s: '',
|
|
signatureType: SignatureType.EthSign,
|
|
v: 1,
|
|
},
|
|
};
|
|
const metatransactionMock = mock(MetaTransaction);
|
|
when(metatransactionMock.getHash()).thenReturn('0xmetatransactionhash');
|
|
when(metatransactionMock.expirationTimeSeconds).thenReturn(NEVER_EXPIRES);
|
|
|
|
const service = buildRfqmServiceForUnitTest({
|
|
chainId: 1,
|
|
dbUtils: instance(dbUtilsMock),
|
|
feeModelVersion: 0,
|
|
});
|
|
|
|
expect(service.submitTakerSignedOtcOrderAsync(params)).to.be.rejectedWith(
|
|
TooManyRequestsError,
|
|
'a pending trade for this taker and takertoken already exists',
|
|
);
|
|
});
|
|
|
|
it('should allow two trades by the same taker with different taker tokens', async () => {
|
|
const takerPrivateKey = '0xe13ae9fa0166b501a2ab50e7b6fbb65819add7376da9b4fbb3bf3ae48cd9dcd3';
|
|
const takerAddress = '0x4e2145eDC29f27E126154B9c716Df70c429C291B';
|
|
const expiry = new BigNumber(Date.now() + 1_000_000).dividedBy(ONE_SECOND_MS).decimalPlaces(0);
|
|
const existingOtcOrder = new OtcOrder({
|
|
txOrigin: '0x0000000000000000000000000000000000000000',
|
|
taker: takerAddress,
|
|
maker: '0x2222222222222222222222222222222222222222',
|
|
makerToken: '0x3333333333333333333333333333333333333333',
|
|
takerToken: '0x4444444444444444444444444444444444444444',
|
|
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(expiry, ZERO, expiry),
|
|
chainId: 1337,
|
|
verifyingContract: '0x0000000000000000000000000000000000000000',
|
|
});
|
|
const newOtcOrder = new OtcOrder({
|
|
txOrigin: '0x0000000000000000000000000000000000000000',
|
|
taker: takerAddress,
|
|
maker: '0x2222222222222222222222222222222222222222',
|
|
makerToken: '0x3333333333333333333333333333333333333333',
|
|
takerToken: '0x9999999999999999999999999999999999999999',
|
|
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(expiry, ZERO, expiry),
|
|
chainId: 1337,
|
|
verifyingContract: '0x0000000000000000000000000000000000000000',
|
|
});
|
|
const existingJob = new RfqmV2JobEntity({
|
|
chainId: 1337,
|
|
expiry,
|
|
makerUri: '',
|
|
orderHash: '0x00',
|
|
fee: {
|
|
token: '0xToken',
|
|
amount: '100',
|
|
type: 'fixed',
|
|
},
|
|
order: otcOrderToStoredOtcOrder(existingOtcOrder),
|
|
workflow: 'rfqm',
|
|
});
|
|
|
|
const dbUtilsMock = mock(RfqmDbUtils);
|
|
when(dbUtilsMock.findV2JobsWithStatusesAsync(anything())).thenResolve([existingJob]);
|
|
when(dbUtilsMock.findV2QuoteByOrderHashAsync(newOtcOrder.getHash())).thenResolve({
|
|
affiliateAddress: '',
|
|
chainId: 1,
|
|
createdAt: new Date(),
|
|
fee: {
|
|
amount: '0',
|
|
token: '',
|
|
type: 'fixed',
|
|
},
|
|
integratorId: '',
|
|
makerUri: 'http://foo.bar',
|
|
order: otcOrderToStoredOtcOrder(newOtcOrder),
|
|
orderHash: '',
|
|
isUnwrap: false,
|
|
takerSpecifiedSide: null,
|
|
makerSignature: null,
|
|
workflow: 'rfqm',
|
|
});
|
|
const metatransactionMock = mock(MetaTransaction);
|
|
when(metatransactionMock.getHash()).thenReturn('0xmetatransactionhash');
|
|
when(metatransactionMock.expirationTimeSeconds).thenReturn(NEVER_EXPIRES);
|
|
const blockchainUtilsMock = mock(RfqBlockchainUtils);
|
|
when(blockchainUtilsMock.getMinOfBalancesAndAllowancesAsync(anything())).thenResolve([
|
|
new BigNumber(10000),
|
|
]);
|
|
const rfqMakerBalanceCacheServiceMock = mock(RfqMakerBalanceCacheService);
|
|
when(rfqMakerBalanceCacheServiceMock.getERC20OwnerBalancesAsync(anything(), anything())).thenResolve([
|
|
new BigNumber(10000),
|
|
]);
|
|
|
|
const service = buildRfqmServiceForUnitTest({
|
|
chainId: 1,
|
|
dbUtils: instance(dbUtilsMock),
|
|
rfqBlockchainUtils: instance(blockchainUtilsMock),
|
|
rfqMakerBalanceCacheService: instance(rfqMakerBalanceCacheServiceMock),
|
|
feeModelVersion: 0,
|
|
});
|
|
|
|
const submitParams: OtcOrderSubmitRfqmSignedQuoteParams = {
|
|
type: GaslessTypes.OtcOrder,
|
|
order: newOtcOrder,
|
|
signature: ethSignHashWithKey(newOtcOrder.getHash(), takerPrivateKey),
|
|
};
|
|
const result = await service.submitTakerSignedOtcOrderAsync(submitParams);
|
|
expect(result.type).to.equal('otc');
|
|
});
|
|
});
|
|
|
|
describe('submitTakerSignedOtcOrderWithApprovalAsync', () => {
|
|
it('should fail if approval params generate an invalid calldata', async () => {
|
|
const takerAddress = '0x4e2145eDC29f27E126154B9c716Df70c429C291B';
|
|
const expiry = new BigNumber(Date.now() + 1_000_000).dividedBy(ONE_SECOND_MS).decimalPlaces(0);
|
|
const otcOrder = new OtcOrder({
|
|
txOrigin: '0x0000000000000000000000000000000000000000',
|
|
taker: '0x1111111111111111111111111111111111111111',
|
|
maker: '0x2222222222222222222222222222222222222222',
|
|
makerToken: '0x3333333333333333333333333333333333333333',
|
|
takerToken: '0x4444444444444444444444444444444444444444',
|
|
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(expiry, ZERO, expiry),
|
|
chainId: 1,
|
|
verifyingContract: '0x0000000000000000000000000000000000000000',
|
|
});
|
|
const eip712Context: ExecuteMetaTransactionEip712Context = {
|
|
types: {
|
|
EIP712Domain: [
|
|
{ name: 'name', type: 'string' },
|
|
{ name: 'version', type: 'string' },
|
|
{ name: 'verifyingContract', type: 'address' },
|
|
{ name: 'salt', type: 'bytes32' },
|
|
],
|
|
MetaTransaction: [
|
|
{ name: 'nonce', type: 'uint256' },
|
|
{ name: 'from', type: 'address' },
|
|
{ name: 'functionSignature', type: 'bytes' },
|
|
],
|
|
},
|
|
primaryType: 'MetaTransaction',
|
|
domain: {},
|
|
message: {
|
|
nonce: expiry.toNumber(),
|
|
from: takerAddress,
|
|
functionSignature: '',
|
|
},
|
|
};
|
|
const submitParams: SubmitRfqmSignedQuoteWithApprovalParams<ExecuteMetaTransactionApproval> = {
|
|
approval: {
|
|
type: GaslessApprovalTypes.ExecuteMetaTransaction,
|
|
eip712: eip712Context,
|
|
signature: {
|
|
r: '',
|
|
s: '',
|
|
v: 28,
|
|
signatureType: SignatureType.EIP712,
|
|
},
|
|
},
|
|
trade: {
|
|
type: GaslessTypes.OtcOrder,
|
|
order: otcOrder,
|
|
signature: {
|
|
r: '',
|
|
s: '',
|
|
v: 28,
|
|
signatureType: SignatureType.EthSign,
|
|
},
|
|
},
|
|
kind: GaslessTypes.OtcOrder,
|
|
};
|
|
const blockchainUtilsMock = mock(RfqBlockchainUtils);
|
|
when(blockchainUtilsMock.generateApprovalCalldataAsync(anything(), anything(), anything())).thenResolve(
|
|
'0xinvalidcalldata',
|
|
);
|
|
when(blockchainUtilsMock.estimateGasForAsync(anything())).thenReject();
|
|
const service = buildRfqmServiceForUnitTest({
|
|
chainId: 1,
|
|
feeModelVersion: 0,
|
|
rfqBlockchainUtils: instance(blockchainUtilsMock),
|
|
});
|
|
try {
|
|
await service.submitTakerSignedOtcOrderWithApprovalAsync(submitParams);
|
|
expect.fail('should fail eth call approval validation');
|
|
} catch (e) {
|
|
expect(e.message).to.contain('Eth call approval validation failed');
|
|
verify(blockchainUtilsMock.generateApprovalCalldataAsync(anything(), anything(), anything())).once();
|
|
verify(blockchainUtilsMock.estimateGasForAsync(anything())).thrice();
|
|
}
|
|
});
|
|
it('should proceed with trade submission if approval is empty', async () => {
|
|
const takerPrivateKey = '0xe13ae9fa0166b501a2ab50e7b6fbb65819add7376da9b4fbb3bf3ae48cd9dcd3';
|
|
const takerAddress = '0x4e2145eDC29f27E126154B9c716Df70c429C291B';
|
|
const expiry = new BigNumber(Date.now() + 1_000_000).dividedBy(ONE_SECOND_MS).decimalPlaces(0);
|
|
const otcOrder = new OtcOrder({
|
|
txOrigin: '0x0000000000000000000000000000000000000000',
|
|
taker: takerAddress,
|
|
maker: '0x2222222222222222222222222222222222222222',
|
|
makerToken: '0x3333333333333333333333333333333333333333',
|
|
takerToken: '0x4444444444444444444444444444444444444444',
|
|
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(expiry, ZERO, expiry),
|
|
chainId: 1,
|
|
verifyingContract: '0x0000000000000000000000000000000000000000',
|
|
});
|
|
const dbUtilsMock = mock(RfqmDbUtils);
|
|
when(dbUtilsMock.findV2JobsWithStatusesAsync(anything())).thenResolve([]);
|
|
when(dbUtilsMock.findV2QuoteByOrderHashAsync(otcOrder.getHash())).thenResolve({
|
|
affiliateAddress: '',
|
|
chainId: 1,
|
|
createdAt: new Date(),
|
|
fee: {
|
|
amount: '0',
|
|
token: '',
|
|
type: 'fixed',
|
|
},
|
|
integratorId: '',
|
|
makerUri: 'http://foo.bar',
|
|
order: otcOrderToStoredOtcOrder(otcOrder),
|
|
orderHash: '',
|
|
isUnwrap: false,
|
|
takerSpecifiedSide: null,
|
|
makerSignature: null,
|
|
workflow: 'rfqm',
|
|
});
|
|
const blockchainUtilsMock = mock(RfqBlockchainUtils);
|
|
when(blockchainUtilsMock.getMinOfBalancesAndAllowancesAsync(anything())).thenResolve([
|
|
new BigNumber(10000),
|
|
]);
|
|
const rfqMakerBalanceCacheServiceMock = mock(RfqMakerBalanceCacheService);
|
|
when(rfqMakerBalanceCacheServiceMock.getERC20OwnerBalancesAsync(anything(), anything())).thenResolve([
|
|
new BigNumber(10000),
|
|
]);
|
|
const service = buildRfqmServiceForUnitTest({
|
|
chainId: 1,
|
|
feeModelVersion: 0,
|
|
dbUtils: instance(dbUtilsMock),
|
|
rfqBlockchainUtils: instance(blockchainUtilsMock),
|
|
rfqMakerBalanceCacheService: instance(rfqMakerBalanceCacheServiceMock),
|
|
});
|
|
const submitParams: SubmitRfqmSignedQuoteWithApprovalParams<Approval> = {
|
|
kind: GaslessTypes.OtcOrder,
|
|
trade: {
|
|
type: GaslessTypes.OtcOrder,
|
|
order: otcOrder,
|
|
signature: ethSignHashWithKey(otcOrder.getHash(), takerPrivateKey),
|
|
},
|
|
};
|
|
const result = await service.submitTakerSignedOtcOrderWithApprovalAsync(submitParams);
|
|
expect(result.type).to.equal('otc');
|
|
verify(dbUtilsMock.writeV2JobAsync(anything())).once();
|
|
});
|
|
it('should save job with executeMetaTransaction params to DB', async () => {
|
|
const takerPrivateKey = '0xe13ae9fa0166b501a2ab50e7b6fbb65819add7376da9b4fbb3bf3ae48cd9dcd3';
|
|
const takerAddress = '0x4e2145eDC29f27E126154B9c716Df70c429C291B';
|
|
const expiry = new BigNumber(Date.now() + 1_000_000).dividedBy(ONE_SECOND_MS).decimalPlaces(0);
|
|
const otcOrder = new OtcOrder({
|
|
txOrigin: '0x0000000000000000000000000000000000000000',
|
|
taker: takerAddress,
|
|
maker: '0x2222222222222222222222222222222222222222',
|
|
makerToken: '0x3333333333333333333333333333333333333333',
|
|
takerToken: '0x4444444444444444444444444444444444444444',
|
|
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(expiry, ZERO, expiry),
|
|
chainId: 1,
|
|
verifyingContract: '0x0000000000000000000000000000000000000000',
|
|
});
|
|
const eip712Context: ExecuteMetaTransactionEip712Context = {
|
|
types: {
|
|
EIP712Domain: [
|
|
{ name: 'name', type: 'string' },
|
|
{ name: 'version', type: 'string' },
|
|
{ name: 'verifyingContract', type: 'address' },
|
|
{ name: 'salt', type: 'bytes32' },
|
|
],
|
|
MetaTransaction: [
|
|
{ name: 'nonce', type: 'uint256' },
|
|
{ name: 'from', type: 'address' },
|
|
{ name: 'functionSignature', type: 'bytes' },
|
|
],
|
|
},
|
|
primaryType: 'MetaTransaction',
|
|
domain: {},
|
|
message: {
|
|
from: takerAddress,
|
|
functionSignature: '',
|
|
nonce: expiry.toNumber(),
|
|
},
|
|
};
|
|
const dbUtilsMock = mock(RfqmDbUtils);
|
|
when(dbUtilsMock.findV2JobsWithStatusesAsync(anything())).thenResolve([]);
|
|
when(dbUtilsMock.findV2QuoteByOrderHashAsync(otcOrder.getHash())).thenResolve({
|
|
affiliateAddress: '',
|
|
chainId: 1,
|
|
createdAt: new Date(),
|
|
fee: {
|
|
amount: '0',
|
|
token: '',
|
|
type: 'fixed',
|
|
},
|
|
integratorId: '',
|
|
makerUri: 'http://foo.bar',
|
|
order: otcOrderToStoredOtcOrder(otcOrder),
|
|
orderHash: '',
|
|
isUnwrap: false,
|
|
takerSpecifiedSide: null,
|
|
makerSignature: null,
|
|
workflow: 'rfqm',
|
|
});
|
|
const blockchainUtilsMock = mock(RfqBlockchainUtils);
|
|
when(blockchainUtilsMock.getTokenBalancesAsync(anything())).thenResolve([new BigNumber(10000)]);
|
|
when(blockchainUtilsMock.generateApprovalCalldataAsync(anything(), anything(), anything())).thenResolve(
|
|
'0xvalidcalldata',
|
|
);
|
|
const rfqMakerBalanceCacheServiceMock = mock(RfqMakerBalanceCacheService);
|
|
when(rfqMakerBalanceCacheServiceMock.getERC20OwnerBalancesAsync(anything(), anything())).thenResolve([
|
|
new BigNumber(10000),
|
|
]);
|
|
const service = buildRfqmServiceForUnitTest({
|
|
chainId: 1,
|
|
feeModelVersion: 0,
|
|
dbUtils: instance(dbUtilsMock),
|
|
rfqBlockchainUtils: instance(blockchainUtilsMock),
|
|
rfqMakerBalanceCacheService: instance(rfqMakerBalanceCacheServiceMock),
|
|
});
|
|
const submitParams: SubmitRfqmSignedQuoteWithApprovalParams<ExecuteMetaTransactionApproval> = {
|
|
trade: {
|
|
type: GaslessTypes.OtcOrder,
|
|
order: otcOrder,
|
|
signature: ethSignHashWithKey(otcOrder.getHash(), takerPrivateKey),
|
|
},
|
|
approval: {
|
|
type: GaslessApprovalTypes.ExecuteMetaTransaction,
|
|
eip712: eip712Context,
|
|
signature: eip712SignHashWithKey(otcOrder.getHash(), takerPrivateKey),
|
|
},
|
|
kind: GaslessTypes.OtcOrder,
|
|
};
|
|
const result = await service.submitTakerSignedOtcOrderWithApprovalAsync(submitParams);
|
|
expect(result.type).to.equal('otc');
|
|
verify(blockchainUtilsMock.getMinOfBalancesAndAllowancesAsync(anything())).never();
|
|
verify(dbUtilsMock.writeV2JobAsync(anything())).once();
|
|
});
|
|
it('should save job with permit params to DB', async () => {
|
|
const takerPrivateKey = '0xe13ae9fa0166b501a2ab50e7b6fbb65819add7376da9b4fbb3bf3ae48cd9dcd3';
|
|
const takerAddress = '0x4e2145eDC29f27E126154B9c716Df70c429C291B';
|
|
const expiry = new BigNumber(Date.now() + 1_000_000).dividedBy(ONE_SECOND_MS).decimalPlaces(0);
|
|
const otcOrder = new OtcOrder({
|
|
txOrigin: '0x0000000000000000000000000000000000000000',
|
|
taker: takerAddress,
|
|
maker: '0x2222222222222222222222222222222222222222',
|
|
makerToken: '0x3333333333333333333333333333333333333333',
|
|
takerToken: '0x4444444444444444444444444444444444444444',
|
|
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(expiry, ZERO, expiry),
|
|
chainId: 1,
|
|
verifyingContract: '0x0000000000000000000000000000000000000000',
|
|
});
|
|
const eip712Context: PermitEip712Context = {
|
|
types: {
|
|
EIP712Domain: [
|
|
{ name: 'name', type: 'string' },
|
|
{ name: 'version', type: 'string' },
|
|
{ name: 'verifyingContract', type: 'address' },
|
|
{ name: 'salt', type: 'bytes32' },
|
|
],
|
|
Permit: [
|
|
{ name: 'owner', type: 'address' },
|
|
{ name: 'spender', type: 'address' },
|
|
{ name: 'value', type: 'uint256' },
|
|
{ name: 'nonce', type: 'uint256' },
|
|
{ name: 'deadline', type: 'uint256' },
|
|
],
|
|
},
|
|
primaryType: 'Permit',
|
|
domain: {},
|
|
message: {
|
|
deadline: '12345',
|
|
owner: takerAddress,
|
|
spender: '0x0000000000000000000000000000000000000000',
|
|
value: '0xffffffffffffffffffffffffffffffffffffffff',
|
|
nonce: expiry.toNumber(),
|
|
},
|
|
};
|
|
const dbUtilsMock = mock(RfqmDbUtils);
|
|
when(dbUtilsMock.findV2JobsWithStatusesAsync(anything())).thenResolve([]);
|
|
when(dbUtilsMock.findV2QuoteByOrderHashAsync(otcOrder.getHash())).thenResolve({
|
|
affiliateAddress: '',
|
|
chainId: 1,
|
|
createdAt: new Date(),
|
|
fee: {
|
|
amount: '0',
|
|
token: '',
|
|
type: 'fixed',
|
|
},
|
|
integratorId: '',
|
|
makerUri: 'http://foo.bar',
|
|
order: otcOrderToStoredOtcOrder(otcOrder),
|
|
orderHash: '',
|
|
isUnwrap: false,
|
|
takerSpecifiedSide: null,
|
|
makerSignature: null,
|
|
workflow: 'rfqm',
|
|
});
|
|
const blockchainUtilsMock = mock(RfqBlockchainUtils);
|
|
when(blockchainUtilsMock.getTokenBalancesAsync(anything())).thenResolve([new BigNumber(10000)]);
|
|
when(blockchainUtilsMock.generateApprovalCalldataAsync(anything(), anything(), anything())).thenResolve(
|
|
'0xvalidcalldata',
|
|
);
|
|
when(blockchainUtilsMock.estimateGasForAsync(anything())).thenResolve(10);
|
|
const rfqMakerBalanceCacheServiceMock = mock(RfqMakerBalanceCacheService);
|
|
when(rfqMakerBalanceCacheServiceMock.getERC20OwnerBalancesAsync(anything(), anything())).thenResolve([
|
|
new BigNumber(10000),
|
|
]);
|
|
const service = buildRfqmServiceForUnitTest({
|
|
chainId: 1,
|
|
feeModelVersion: 0,
|
|
dbUtils: instance(dbUtilsMock),
|
|
rfqBlockchainUtils: instance(blockchainUtilsMock),
|
|
rfqMakerBalanceCacheService: instance(rfqMakerBalanceCacheServiceMock),
|
|
});
|
|
const submitParams: SubmitRfqmSignedQuoteWithApprovalParams<PermitApproval> = {
|
|
trade: {
|
|
type: GaslessTypes.OtcOrder,
|
|
order: otcOrder,
|
|
signature: ethSignHashWithKey(otcOrder.getHash(), takerPrivateKey),
|
|
},
|
|
approval: {
|
|
type: GaslessApprovalTypes.Permit,
|
|
eip712: eip712Context,
|
|
signature: eip712SignHashWithKey(otcOrder.getHash(), takerPrivateKey),
|
|
},
|
|
kind: GaslessTypes.OtcOrder,
|
|
};
|
|
const result = await service.submitTakerSignedOtcOrderWithApprovalAsync(submitParams);
|
|
expect(result.type).to.equal('otc');
|
|
verify(blockchainUtilsMock.getMinOfBalancesAndAllowancesAsync(anything())).never();
|
|
verify(dbUtilsMock.writeV2JobAsync(anything())).once();
|
|
});
|
|
});
|
|
|
|
describe('fetchIndicativeQuoteAsync', () => {
|
|
describe('sells', () => {
|
|
it('should fetch indicative quote', async () => {
|
|
const contractAddresses = getContractAddressesForChainOrThrow(1);
|
|
|
|
const quote: IndicativeQuote = {
|
|
maker: '0xmaker',
|
|
makerToken: contractAddresses.zrxToken,
|
|
makerAmount: new BigNumber(101),
|
|
takerToken: contractAddresses.etherToken,
|
|
takerAmount: new BigNumber(100),
|
|
expiry: NEVER_EXPIRES,
|
|
makerUri: MOCK_MM_URI,
|
|
};
|
|
const quoteServerClientMock = mock(QuoteServerClient);
|
|
when(quoteServerClientMock.batchGetPriceV2Async(anything(), anything(), anything())).thenResolve([
|
|
quote,
|
|
]);
|
|
|
|
const service = buildRfqmServiceForUnitTest({
|
|
quoteServerClient: instance(quoteServerClientMock),
|
|
feeModelVersion: 0,
|
|
});
|
|
|
|
const res = await service.fetchIndicativeQuoteAsync({
|
|
integrator: MOCK_INTEGRATOR,
|
|
buyToken: contractAddresses.zrxToken,
|
|
sellToken: contractAddresses.etherToken,
|
|
buyTokenDecimals: 18,
|
|
sellTokenDecimals: 18,
|
|
sellAmount: new BigNumber(100),
|
|
});
|
|
|
|
if (res === null) {
|
|
expect.fail('res is null, but not expected to be null');
|
|
return;
|
|
}
|
|
expect(res.sellAmount.toNumber()).to.be.at.least(100);
|
|
expect(res.price.toNumber()).to.equal(1.01);
|
|
});
|
|
|
|
it('should round price to six decimal places', async () => {
|
|
const contractAddresses = getContractAddressesForChainOrThrow(1);
|
|
const quote: IndicativeQuote = {
|
|
maker: '0xmaker',
|
|
makerToken: contractAddresses.zrxToken,
|
|
makerAmount: new BigNumber(111),
|
|
takerToken: contractAddresses.etherToken,
|
|
takerAmount: new BigNumber(333),
|
|
expiry: NEVER_EXPIRES,
|
|
makerUri: MOCK_MM_URI,
|
|
};
|
|
const quoteServerClientMock = mock(QuoteServerClient);
|
|
when(quoteServerClientMock.batchGetPriceV2Async(anything(), anything(), anything())).thenResolve([
|
|
quote,
|
|
]);
|
|
|
|
const service = buildRfqmServiceForUnitTest({
|
|
quoteServerClient: instance(quoteServerClientMock),
|
|
feeModelVersion: 0,
|
|
});
|
|
|
|
const res = await service.fetchIndicativeQuoteAsync({
|
|
integrator: MOCK_INTEGRATOR,
|
|
buyToken: contractAddresses.zrxToken,
|
|
sellToken: contractAddresses.etherToken,
|
|
buyTokenDecimals: 18,
|
|
sellTokenDecimals: 18,
|
|
sellAmount: new BigNumber(333),
|
|
});
|
|
|
|
if (res === null) {
|
|
expect.fail('res is null, but not expected to be null');
|
|
return;
|
|
}
|
|
|
|
expect(res.price.toNumber()).to.equal(0.3333333);
|
|
});
|
|
|
|
it('should only return an indicative quote that is 100% filled when selling', async () => {
|
|
const contractAddresses = getContractAddressesForChainOrThrow(1);
|
|
const quote1: IndicativeQuote = {
|
|
maker: '0xmaker',
|
|
makerToken: contractAddresses.zrxToken,
|
|
makerAmount: new BigNumber(55),
|
|
takerToken: contractAddresses.etherToken,
|
|
takerAmount: new BigNumber(50),
|
|
expiry: NEVER_EXPIRES,
|
|
makerUri: MOCK_MM_URI,
|
|
};
|
|
const quote2: IndicativeQuote = {
|
|
maker: '0xmaker',
|
|
makerToken: contractAddresses.zrxToken,
|
|
makerAmount: new BigNumber(105),
|
|
takerToken: contractAddresses.etherToken,
|
|
takerAmount: new BigNumber(100),
|
|
expiry: NEVER_EXPIRES,
|
|
makerUri: MOCK_MM_URI,
|
|
};
|
|
const quoteServerClientMock = mock(QuoteServerClient);
|
|
when(quoteServerClientMock.batchGetPriceV2Async(anything(), anything(), anything())).thenResolve([
|
|
quote1,
|
|
quote2,
|
|
]);
|
|
const service = buildRfqmServiceForUnitTest({ quoteServerClient: instance(quoteServerClientMock) });
|
|
|
|
const res = await service.fetchIndicativeQuoteAsync({
|
|
integrator: MOCK_INTEGRATOR,
|
|
buyToken: contractAddresses.zrxToken,
|
|
sellToken: contractAddresses.etherToken,
|
|
buyTokenDecimals: 18,
|
|
sellTokenDecimals: 18,
|
|
sellAmount: new BigNumber(100),
|
|
});
|
|
|
|
if (res === null) {
|
|
expect.fail('res is null, but not expected to be null');
|
|
return;
|
|
}
|
|
expect(res.sellAmount.toNumber()).to.equal(100);
|
|
expect(res.price.toNumber()).to.equal(1.05);
|
|
});
|
|
|
|
it('should return null if no quotes are valid', async () => {
|
|
const contractAddresses = getContractAddressesForChainOrThrow(1);
|
|
const partialFillQuote: IndicativeQuote = {
|
|
maker: '0xmaker',
|
|
makerToken: contractAddresses.zrxToken,
|
|
makerAmount: new BigNumber(55),
|
|
takerToken: contractAddresses.etherToken,
|
|
takerAmount: new BigNumber(50),
|
|
expiry: NEVER_EXPIRES,
|
|
makerUri: MOCK_MM_URI,
|
|
};
|
|
const quoteServerClientMock = mock(QuoteServerClient);
|
|
when(quoteServerClientMock.batchGetPriceV2Async(anything(), anything(), anything())).thenResolve([
|
|
partialFillQuote,
|
|
]);
|
|
const service = buildRfqmServiceForUnitTest({ quoteServerClient: instance(quoteServerClientMock) });
|
|
|
|
const res = await service.fetchIndicativeQuoteAsync({
|
|
integrator: MOCK_INTEGRATOR,
|
|
buyToken: contractAddresses.zrxToken,
|
|
sellToken: contractAddresses.etherToken,
|
|
buyTokenDecimals: 18,
|
|
sellTokenDecimals: 18,
|
|
sellAmount: new BigNumber(100),
|
|
});
|
|
expect(res).to.equal(null);
|
|
});
|
|
|
|
// TODO: we may want to reintroduce this test very soon. However, if not addressed by June 2022, remove
|
|
it.skip('should return an indicative quote that can fill more than 100%', async () => {
|
|
const contractAddresses = getContractAddressesForChainOrThrow(1);
|
|
const worseQuote: IndicativeQuote = {
|
|
maker: '0xmaker',
|
|
makerToken: contractAddresses.zrxToken,
|
|
makerAmount: new BigNumber(101),
|
|
takerToken: contractAddresses.etherToken,
|
|
takerAmount: new BigNumber(100),
|
|
expiry: NEVER_EXPIRES,
|
|
makerUri: MOCK_MM_URI,
|
|
};
|
|
const betterQuote: IndicativeQuote = {
|
|
maker: '0xmaker2',
|
|
makerToken: contractAddresses.zrxToken,
|
|
makerAmount: new BigNumber(222),
|
|
takerToken: contractAddresses.etherToken,
|
|
takerAmount: new BigNumber(200),
|
|
expiry: NEVER_EXPIRES,
|
|
makerUri: MOCK_MM_URI,
|
|
};
|
|
const quoteServerClientMock = mock(QuoteServerClient);
|
|
when(quoteServerClientMock.batchGetPriceV2Async(anything(), anything(), anything())).thenResolve([
|
|
worseQuote,
|
|
betterQuote,
|
|
]);
|
|
const service = buildRfqmServiceForUnitTest({ quoteServerClient: instance(quoteServerClientMock) });
|
|
|
|
const res = await service.fetchIndicativeQuoteAsync({
|
|
integrator: MOCK_INTEGRATOR,
|
|
buyToken: contractAddresses.zrxToken,
|
|
sellToken: contractAddresses.etherToken,
|
|
buyTokenDecimals: 18,
|
|
sellTokenDecimals: 18,
|
|
sellAmount: new BigNumber(100),
|
|
});
|
|
|
|
if (res === null) {
|
|
expect.fail('res is null, but not expected to be null');
|
|
return;
|
|
}
|
|
expect(res.sellAmount.toNumber()).to.equal(200);
|
|
expect(res.price.toNumber()).to.equal(1.11);
|
|
});
|
|
|
|
it('should ignore quotes that are for the wrong pair', async () => {
|
|
const contractAddresses = getContractAddressesForChainOrThrow(1);
|
|
const worseQuote: IndicativeQuote = {
|
|
maker: '0xmaker',
|
|
makerToken: contractAddresses.zrxToken,
|
|
makerAmount: new BigNumber(101),
|
|
takerToken: contractAddresses.etherToken,
|
|
takerAmount: new BigNumber(100),
|
|
expiry: NEVER_EXPIRES,
|
|
makerUri: MOCK_MM_URI,
|
|
};
|
|
const betterQuote: IndicativeQuote = {
|
|
maker: '0xmaker2',
|
|
makerToken: '0x1111111111111111111111111111111111111111',
|
|
makerAmount: new BigNumber(111),
|
|
takerToken: contractAddresses.etherToken,
|
|
takerAmount: new BigNumber(100),
|
|
expiry: NEVER_EXPIRES,
|
|
makerUri: MOCK_MM_URI,
|
|
};
|
|
const quoteServerClientMock = mock(QuoteServerClient);
|
|
when(quoteServerClientMock.batchGetPriceV2Async(anything(), anything(), anything())).thenResolve([
|
|
worseQuote,
|
|
betterQuote,
|
|
]);
|
|
const service = buildRfqmServiceForUnitTest({ quoteServerClient: instance(quoteServerClientMock) });
|
|
|
|
const res = await service.fetchIndicativeQuoteAsync({
|
|
integrator: MOCK_INTEGRATOR,
|
|
buyToken: contractAddresses.zrxToken,
|
|
sellToken: contractAddresses.etherToken,
|
|
buyTokenDecimals: 18,
|
|
sellTokenDecimals: 18,
|
|
sellAmount: new BigNumber(100),
|
|
});
|
|
|
|
if (res === null) {
|
|
expect.fail('res is null, but not expected to be null');
|
|
return;
|
|
}
|
|
expect(res.sellAmount.toNumber()).to.equal(100);
|
|
expect(res.price.toNumber()).to.equal(1.01); // Worse pricing wins because better pricing is for wrong pair
|
|
});
|
|
|
|
it('should ignore quotes that expire within 3 minutes', async () => {
|
|
const contractAddresses = getContractAddressesForChainOrThrow(1);
|
|
const inOneMinute = (Date.now() + ONE_MINUTE_MS) / ONE_SECOND_MS;
|
|
const expiresSoon: IndicativeQuote = {
|
|
maker: '0xmaker',
|
|
makerToken: contractAddresses.zrxToken,
|
|
makerAmount: new BigNumber(111),
|
|
takerToken: contractAddresses.etherToken,
|
|
takerAmount: new BigNumber(100),
|
|
expiry: new BigNumber(inOneMinute),
|
|
makerUri: MOCK_MM_URI,
|
|
};
|
|
const neverExpires: IndicativeQuote = {
|
|
maker: '0xmaker2',
|
|
makerToken: contractAddresses.zrxToken,
|
|
makerAmount: new BigNumber(101),
|
|
takerToken: contractAddresses.etherToken,
|
|
takerAmount: new BigNumber(100),
|
|
expiry: NEVER_EXPIRES,
|
|
makerUri: MOCK_MM_URI,
|
|
};
|
|
const quoteServerClientMock = mock(QuoteServerClient);
|
|
when(quoteServerClientMock.batchGetPriceV2Async(anything(), anything(), anything())).thenResolve([
|
|
expiresSoon,
|
|
neverExpires,
|
|
]);
|
|
const service = buildRfqmServiceForUnitTest({ quoteServerClient: instance(quoteServerClientMock) });
|
|
|
|
const res = await service.fetchIndicativeQuoteAsync({
|
|
integrator: MOCK_INTEGRATOR,
|
|
buyToken: contractAddresses.zrxToken,
|
|
sellToken: contractAddresses.etherToken,
|
|
buyTokenDecimals: 18,
|
|
sellTokenDecimals: 18,
|
|
sellAmount: new BigNumber(100),
|
|
});
|
|
|
|
if (res === null) {
|
|
expect.fail('res is null, but not expected to be null');
|
|
return;
|
|
}
|
|
expect(res.sellAmount.toNumber()).to.equal(100);
|
|
expect(res.price.toNumber()).to.equal(1.01); // Worse pricing wins because better pricing expires too soon
|
|
});
|
|
});
|
|
|
|
describe('buys', () => {
|
|
it('should fetch indicative quote when buying', async () => {
|
|
const contractAddresses = getContractAddressesForChainOrThrow(1);
|
|
const quote: IndicativeQuote = {
|
|
maker: '0xmaker',
|
|
makerToken: contractAddresses.zrxToken,
|
|
makerAmount: new BigNumber(100),
|
|
takerToken: contractAddresses.etherToken,
|
|
takerAmount: new BigNumber(80),
|
|
expiry: NEVER_EXPIRES,
|
|
makerUri: MOCK_MM_URI,
|
|
};
|
|
const quoteServerClientMock = mock(QuoteServerClient);
|
|
when(quoteServerClientMock.batchGetPriceV2Async(anything(), anything(), anything())).thenResolve([
|
|
quote,
|
|
]);
|
|
const service = buildRfqmServiceForUnitTest({ quoteServerClient: instance(quoteServerClientMock) });
|
|
|
|
const res = await service.fetchIndicativeQuoteAsync({
|
|
integrator: MOCK_INTEGRATOR,
|
|
buyToken: contractAddresses.zrxToken,
|
|
sellToken: contractAddresses.etherToken,
|
|
buyTokenDecimals: 18,
|
|
sellTokenDecimals: 18,
|
|
buyAmount: new BigNumber(100),
|
|
});
|
|
|
|
if (res === null) {
|
|
expect.fail('res is null, but not expected to be null');
|
|
return;
|
|
}
|
|
expect(res.buyAmount.toNumber()).to.be.at.least(100);
|
|
expect(res.price.toNumber()).to.equal(0.8);
|
|
});
|
|
|
|
it('should only return an indicative quote that is 100% filled when buying', async () => {
|
|
const contractAddresses = getContractAddressesForChainOrThrow(1);
|
|
const overFillQuoteGoodPricing: IndicativeQuote = {
|
|
maker: '0xmaker',
|
|
makerToken: contractAddresses.zrxToken,
|
|
makerAmount: new BigNumber(160),
|
|
takerToken: contractAddresses.etherToken,
|
|
takerAmount: new BigNumber(80),
|
|
expiry: NEVER_EXPIRES,
|
|
makerUri: MOCK_MM_URI,
|
|
};
|
|
const partialFillQuoteGoodPricing: IndicativeQuote = {
|
|
maker: '0xmaker2',
|
|
makerToken: contractAddresses.zrxToken,
|
|
makerAmount: new BigNumber(80),
|
|
takerToken: contractAddresses.etherToken,
|
|
takerAmount: new BigNumber(40),
|
|
expiry: NEVER_EXPIRES,
|
|
makerUri: MOCK_MM_URI,
|
|
};
|
|
const fullQuote: IndicativeQuote = {
|
|
maker: '0xmaker3',
|
|
makerToken: contractAddresses.zrxToken,
|
|
makerAmount: new BigNumber(100),
|
|
takerToken: contractAddresses.etherToken,
|
|
takerAmount: new BigNumber(80),
|
|
expiry: NEVER_EXPIRES,
|
|
makerUri: MOCK_MM_URI,
|
|
};
|
|
const quoteServerClientMock = mock(QuoteServerClient);
|
|
when(quoteServerClientMock.batchGetPriceV2Async(anything(), anything(), anything())).thenResolve([
|
|
overFillQuoteGoodPricing,
|
|
partialFillQuoteGoodPricing,
|
|
fullQuote,
|
|
]);
|
|
const service = buildRfqmServiceForUnitTest({ quoteServerClient: instance(quoteServerClientMock) });
|
|
|
|
const res = await service.fetchIndicativeQuoteAsync({
|
|
integrator: MOCK_INTEGRATOR,
|
|
buyToken: contractAddresses.zrxToken,
|
|
sellToken: contractAddresses.etherToken,
|
|
buyTokenDecimals: 18,
|
|
sellTokenDecimals: 18,
|
|
buyAmount: new BigNumber(100),
|
|
});
|
|
|
|
if (res === null) {
|
|
expect.fail('res is null, but not expected to be null');
|
|
return;
|
|
}
|
|
expect(res.buyAmount.toNumber()).to.equal(100);
|
|
expect(res.price.toNumber()).to.equal(0.8);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('fetchFirmQuoteAsync', () => {
|
|
const takerAddress = '0xf003A9418DE2620f935181259C0Fa1595E871234';
|
|
|
|
it('should use an affiliate address provided in the quote request even if one is present in configuration', async () => {
|
|
const sellAmount = new BigNumber(100);
|
|
const contractAddresses = getContractAddressesForChainOrThrow(1);
|
|
const quote: IndicativeQuote = {
|
|
maker: '0x64B92f5d9E5b5f20603de8498385c3a3d3048E22',
|
|
makerToken: contractAddresses.zrxToken,
|
|
makerAmount: new BigNumber(101),
|
|
takerToken: contractAddresses.etherToken,
|
|
takerAmount: new BigNumber(100),
|
|
expiry: NEVER_EXPIRES,
|
|
makerUri: MOCK_MM_URI,
|
|
};
|
|
|
|
const cacheClientMock = mock(CacheClient);
|
|
when(cacheClientMock.getNextOtcOrderBucketAsync(1337)).thenResolve(420);
|
|
when(cacheClientMock.getMakersInCooldownForPairAsync(anything(), anything(), anything())).thenResolve([]);
|
|
|
|
// Mock out the dbUtils
|
|
const dbUtilsMock = mock(RfqmDbUtils);
|
|
when(dbUtilsMock.writeV2QuoteAsync(anything())).thenResolve();
|
|
const dbUtils = instance(dbUtilsMock);
|
|
|
|
const quoteServerClientMock = mock(QuoteServerClient);
|
|
when(quoteServerClientMock.batchGetPriceV2Async(anything(), anything(), anything())).thenResolve([quote]);
|
|
const rfqMakerBalanceCacheServiceMock = mock(RfqMakerBalanceCacheService);
|
|
when(rfqMakerBalanceCacheServiceMock.getERC20OwnerBalancesAsync(anything(), anything())).thenResolve([
|
|
new BigNumber(150),
|
|
]);
|
|
|
|
const service = buildRfqmServiceForUnitTest({
|
|
quoteServerClient: instance(quoteServerClientMock),
|
|
dbUtils,
|
|
cacheClient: instance(cacheClientMock),
|
|
rfqMakerBalanceCacheService: instance(rfqMakerBalanceCacheServiceMock),
|
|
});
|
|
|
|
await service.fetchFirmQuoteAsync({
|
|
affiliateAddress: '0xaffiliateAddress',
|
|
buyToken: contractAddresses.zrxToken,
|
|
buyTokenDecimals: 18,
|
|
checkApproval: false,
|
|
integrator: { ...MOCK_INTEGRATOR, affiliateAddress: '0xaffiliateAddressNotThisOne' },
|
|
sellAmount,
|
|
sellToken: contractAddresses.etherToken,
|
|
sellTokenDecimals: 18,
|
|
takerAddress,
|
|
});
|
|
|
|
const writeV2QuoteArgs = capture(dbUtilsMock.writeV2QuoteAsync).last();
|
|
expect(writeV2QuoteArgs[0]['affiliateAddress']).to.equal('0xaffiliateAddress');
|
|
});
|
|
|
|
it('should use a configured affiliate address when none is provide in the quote request', async () => {
|
|
const sellAmount = new BigNumber(100);
|
|
const contractAddresses = getContractAddressesForChainOrThrow(1);
|
|
const quote: IndicativeQuote = {
|
|
maker: '0x64B92f5d9E5b5f20603de8498385c3a3d3048E22',
|
|
makerToken: contractAddresses.zrxToken,
|
|
makerAmount: new BigNumber(101),
|
|
takerToken: contractAddresses.etherToken,
|
|
takerAmount: new BigNumber(100),
|
|
expiry: NEVER_EXPIRES,
|
|
makerUri: MOCK_MM_URI,
|
|
};
|
|
|
|
const cacheClientMock = mock(CacheClient);
|
|
when(cacheClientMock.getNextOtcOrderBucketAsync(1337)).thenResolve(420);
|
|
when(cacheClientMock.getMakersInCooldownForPairAsync(anything(), anything(), anything())).thenResolve([]);
|
|
|
|
// Mock out the dbUtils
|
|
const dbUtilsMock = mock(RfqmDbUtils);
|
|
when(dbUtilsMock.writeV2QuoteAsync(anything())).thenResolve();
|
|
const dbUtils = instance(dbUtilsMock);
|
|
|
|
const quoteServerClientMock = mock(QuoteServerClient);
|
|
when(quoteServerClientMock.batchGetPriceV2Async(anything(), anything(), anything())).thenResolve([quote]);
|
|
const rfqMakerBalanceCacheServiceMock = mock(RfqMakerBalanceCacheService);
|
|
when(rfqMakerBalanceCacheServiceMock.getERC20OwnerBalancesAsync(anything(), anything())).thenResolve([
|
|
new BigNumber(150),
|
|
]);
|
|
|
|
const service = buildRfqmServiceForUnitTest({
|
|
quoteServerClient: instance(quoteServerClientMock),
|
|
dbUtils,
|
|
cacheClient: instance(cacheClientMock),
|
|
rfqMakerBalanceCacheService: instance(rfqMakerBalanceCacheServiceMock),
|
|
});
|
|
|
|
await service.fetchFirmQuoteAsync({
|
|
buyToken: contractAddresses.zrxToken,
|
|
buyTokenDecimals: 18,
|
|
checkApproval: false,
|
|
integrator: { ...MOCK_INTEGRATOR, affiliateAddress: '0xaffiliateAddress' },
|
|
sellAmount,
|
|
sellToken: contractAddresses.etherToken,
|
|
sellTokenDecimals: 18,
|
|
takerAddress,
|
|
});
|
|
|
|
const writeV2QuoteArgs = capture(dbUtilsMock.writeV2QuoteAsync).last();
|
|
expect(writeV2QuoteArgs[0]['affiliateAddress']).to.equal('0xaffiliateAddress');
|
|
});
|
|
|
|
describe('sells', () => {
|
|
it('should fetch a firm quote', async () => {
|
|
const sellAmount = new BigNumber(100);
|
|
const contractAddresses = getContractAddressesForChainOrThrow(1);
|
|
const quote: IndicativeQuote = {
|
|
maker: '0x64B92f5d9E5b5f20603de8498385c3a3d3048E22',
|
|
makerToken: contractAddresses.zrxToken,
|
|
makerAmount: new BigNumber(101),
|
|
takerToken: contractAddresses.etherToken,
|
|
takerAmount: new BigNumber(100),
|
|
expiry: NEVER_EXPIRES,
|
|
makerUri: MOCK_MM_URI,
|
|
};
|
|
|
|
const cacheClientMock = mock(CacheClient);
|
|
when(cacheClientMock.getNextOtcOrderBucketAsync(1337)).thenResolve(420);
|
|
when(cacheClientMock.getMakersInCooldownForPairAsync(anything(), anything(), anything())).thenResolve(
|
|
[],
|
|
);
|
|
|
|
// Mock out the dbUtils
|
|
const dbUtilsMock = mock(RfqmDbUtils);
|
|
when(dbUtilsMock.writeV2QuoteAsync(anything())).thenResolve();
|
|
const dbUtils = instance(dbUtilsMock);
|
|
|
|
const quoteServerClientMock = mock(QuoteServerClient);
|
|
when(quoteServerClientMock.batchGetPriceV2Async(anything(), anything(), anything())).thenResolve([
|
|
quote,
|
|
]);
|
|
const rfqMakerBalanceCacheServiceMock = mock(RfqMakerBalanceCacheService);
|
|
when(rfqMakerBalanceCacheServiceMock.getERC20OwnerBalancesAsync(anything(), anything())).thenResolve([
|
|
new BigNumber(150),
|
|
]);
|
|
|
|
const service = buildRfqmServiceForUnitTest({
|
|
quoteServerClient: instance(quoteServerClientMock),
|
|
dbUtils,
|
|
cacheClient: instance(cacheClientMock),
|
|
rfqMakerBalanceCacheService: instance(rfqMakerBalanceCacheServiceMock),
|
|
});
|
|
|
|
const { quote: res } = await service.fetchFirmQuoteAsync({
|
|
integrator: MOCK_INTEGRATOR,
|
|
takerAddress,
|
|
buyToken: contractAddresses.zrxToken,
|
|
sellToken: contractAddresses.etherToken,
|
|
buyTokenDecimals: 18,
|
|
sellTokenDecimals: 18,
|
|
sellAmount,
|
|
checkApproval: false,
|
|
});
|
|
|
|
expect(res).to.exist;
|
|
expect(res?.type).to.equal(GaslessTypes.OtcOrder);
|
|
|
|
expect(res?.sellAmount).to.equal(sellAmount);
|
|
expect(res?.price.toNumber()).to.equal(1.01);
|
|
expect(res?.orderHash).to.match(/^0x[0-9a-fA-F]+/);
|
|
});
|
|
|
|
// TODO: we may want to reintroduce this test very soon. However, if not addressed by June 2022, remove
|
|
it.skip('should scale a firm quote if MM returns too much', async () => {
|
|
const sellAmount = new BigNumber(100);
|
|
const contractAddresses = getContractAddressesForChainOrThrow(1);
|
|
const quote: IndicativeQuote = {
|
|
maker: '0x64B92f5d9E5b5f20603de8498385c3a3d3048E22',
|
|
makerToken: contractAddresses.zrxToken,
|
|
makerAmount: new BigNumber(202),
|
|
takerToken: contractAddresses.etherToken,
|
|
takerAmount: new BigNumber(200),
|
|
expiry: NEVER_EXPIRES,
|
|
makerUri: MOCK_MM_URI,
|
|
};
|
|
|
|
const cacheClientMock = mock(CacheClient);
|
|
when(cacheClientMock.getNextOtcOrderBucketAsync(1337)).thenResolve(420);
|
|
when(cacheClientMock.getMakersInCooldownForPairAsync(anything(), anything(), anything())).thenResolve(
|
|
[],
|
|
);
|
|
|
|
// Mock out the dbUtils
|
|
const dbUtilsMock = mock(RfqmDbUtils);
|
|
when(dbUtilsMock.writeV2QuoteAsync(anything())).thenResolve();
|
|
const dbUtils = instance(dbUtilsMock);
|
|
|
|
const quoteServerClientMock = mock(QuoteServerClient);
|
|
when(quoteServerClientMock.batchGetPriceV2Async(anything(), anything(), anything())).thenResolve([
|
|
quote,
|
|
]);
|
|
const rfqMakerBalanceCacheServiceMock = mock(RfqMakerBalanceCacheService);
|
|
when(rfqMakerBalanceCacheServiceMock.getERC20OwnerBalancesAsync(anything(), anything())).thenResolve([
|
|
new BigNumber(150),
|
|
]);
|
|
|
|
const service = buildRfqmServiceForUnitTest({
|
|
quoteServerClient: instance(quoteServerClientMock),
|
|
dbUtils,
|
|
cacheClient: instance(cacheClientMock),
|
|
rfqMakerBalanceCacheService: instance(rfqMakerBalanceCacheServiceMock),
|
|
});
|
|
|
|
const { quote: res } = await service.fetchFirmQuoteAsync({
|
|
integrator: MOCK_INTEGRATOR,
|
|
takerAddress,
|
|
buyToken: contractAddresses.zrxToken,
|
|
sellToken: contractAddresses.etherToken,
|
|
buyTokenDecimals: 18,
|
|
sellTokenDecimals: 18,
|
|
sellAmount,
|
|
checkApproval: false,
|
|
});
|
|
|
|
expect(res).to.exist;
|
|
expect(res?.type).to.equal(GaslessTypes.OtcOrder);
|
|
expect(res?.sellAmount).to.equal(sellAmount);
|
|
expect(res?.buyAmount.toNumber()).to.equal(101); // result is scaled
|
|
expect(res?.price.toNumber()).to.equal(1.01);
|
|
expect(res?.orderHash).to.match(/^0x[0-9a-fA-F]+/);
|
|
});
|
|
|
|
it('should round price to six decimal places', async () => {
|
|
const contractAddresses = getContractAddressesForChainOrThrow(1);
|
|
const quote: IndicativeQuote = {
|
|
maker: '0x64B92f5d9E5b5f20603de8498385c3a3d3048E22',
|
|
makerToken: contractAddresses.zrxToken,
|
|
makerAmount: new BigNumber(111),
|
|
takerToken: contractAddresses.etherToken,
|
|
takerAmount: new BigNumber(333),
|
|
expiry: NEVER_EXPIRES,
|
|
makerUri: MOCK_MM_URI,
|
|
};
|
|
|
|
const cacheClientMock = mock(CacheClient);
|
|
when(cacheClientMock.getNextOtcOrderBucketAsync(1337)).thenResolve(420);
|
|
when(cacheClientMock.getMakersInCooldownForPairAsync(anything(), anything(), anything())).thenResolve(
|
|
[],
|
|
);
|
|
|
|
// Mock out the dbUtils
|
|
const dbUtilsMock = mock(RfqmDbUtils);
|
|
when(dbUtilsMock.writeV2QuoteAsync(anything())).thenResolve();
|
|
const dbUtils = instance(dbUtilsMock);
|
|
|
|
const quoteServerClientMock = mock(QuoteServerClient);
|
|
when(quoteServerClientMock.batchGetPriceV2Async(anything(), anything(), anything())).thenResolve([
|
|
quote,
|
|
]);
|
|
const rfqMakerBalanceCacheServiceMock = mock(RfqMakerBalanceCacheService);
|
|
when(rfqMakerBalanceCacheServiceMock.getERC20OwnerBalancesAsync(anything(), anything())).thenResolve([
|
|
new BigNumber(150),
|
|
]);
|
|
|
|
const service = buildRfqmServiceForUnitTest({
|
|
quoteServerClient: instance(quoteServerClientMock),
|
|
dbUtils,
|
|
cacheClient: instance(cacheClientMock),
|
|
rfqMakerBalanceCacheService: instance(rfqMakerBalanceCacheServiceMock),
|
|
});
|
|
|
|
const { quote: res } = await service.fetchFirmQuoteAsync({
|
|
integrator: MOCK_INTEGRATOR,
|
|
takerAddress,
|
|
buyToken: contractAddresses.zrxToken,
|
|
sellToken: contractAddresses.etherToken,
|
|
buyTokenDecimals: 18,
|
|
sellTokenDecimals: 18,
|
|
sellAmount: new BigNumber(333),
|
|
checkApproval: false,
|
|
});
|
|
|
|
if (res === null) {
|
|
expect.fail('res is null, but not expected to be null');
|
|
return;
|
|
}
|
|
|
|
expect(res.price.toNumber()).to.equal(0.3333333);
|
|
});
|
|
|
|
it('should not call `getGaslessApprovalResponseAsync` if checkApproval is false', async () => {
|
|
const sellAmount = new BigNumber(100);
|
|
const contractAddresses = getContractAddressesForChainOrThrow(1);
|
|
const quote: IndicativeQuote = {
|
|
maker: '0x64B92f5d9E5b5f20603de8498385c3a3d3048E22',
|
|
makerToken: contractAddresses.zrxToken,
|
|
makerAmount: new BigNumber(101),
|
|
takerToken: contractAddresses.etherToken,
|
|
takerAmount: new BigNumber(100),
|
|
expiry: NEVER_EXPIRES,
|
|
makerUri: MOCK_MM_URI,
|
|
};
|
|
|
|
const cacheClientMock = mock(CacheClient);
|
|
when(cacheClientMock.getNextOtcOrderBucketAsync(1337)).thenResolve(420);
|
|
when(cacheClientMock.getMakersInCooldownForPairAsync(anything(), anything(), anything())).thenResolve(
|
|
[],
|
|
);
|
|
|
|
// Mock out the dbUtils
|
|
const dbUtilsMock = mock(RfqmDbUtils);
|
|
when(dbUtilsMock.writeV2QuoteAsync(anything())).thenResolve();
|
|
const dbUtils = instance(dbUtilsMock);
|
|
|
|
const quoteServerClientMock = mock(QuoteServerClient);
|
|
when(quoteServerClientMock.batchGetPriceV2Async(anything(), anything(), anything())).thenResolve([
|
|
quote,
|
|
]);
|
|
const rfqMakerBalanceCacheServiceMock = mock(RfqMakerBalanceCacheService);
|
|
when(rfqMakerBalanceCacheServiceMock.getERC20OwnerBalancesAsync(anything(), anything())).thenResolve([
|
|
new BigNumber(150),
|
|
]);
|
|
|
|
const service = buildRfqmServiceForUnitTest({
|
|
quoteServerClient: instance(quoteServerClientMock),
|
|
dbUtils,
|
|
cacheClient: instance(cacheClientMock),
|
|
rfqMakerBalanceCacheService: instance(rfqMakerBalanceCacheServiceMock),
|
|
});
|
|
const spiedService = spy(service);
|
|
when(spiedService.getGaslessApprovalResponseAsync(anything(), anything(), anything())).thenThrow(
|
|
new Error('`getGaslessApprovalResponseAsync` should not be called'),
|
|
);
|
|
|
|
const { quote: res } = await service.fetchFirmQuoteAsync({
|
|
integrator: MOCK_INTEGRATOR,
|
|
takerAddress,
|
|
buyToken: contractAddresses.zrxToken,
|
|
sellToken: contractAddresses.etherToken,
|
|
buyTokenDecimals: 18,
|
|
sellTokenDecimals: 18,
|
|
sellAmount,
|
|
checkApproval: false,
|
|
});
|
|
|
|
expect(res).to.exist;
|
|
expect(res?.type).to.equal(GaslessTypes.OtcOrder);
|
|
|
|
expect(res?.sellAmount).to.equal(sellAmount);
|
|
expect(res?.price.toNumber()).to.equal(1.01);
|
|
expect(res?.orderHash).to.match(/^0x[0-9a-fA-F]+/);
|
|
expect(res?.approval).to.equal(undefined);
|
|
});
|
|
|
|
it('should return the correct approval if checkApproval is true', async () => {
|
|
const sellAmount = new BigNumber(100);
|
|
const contractAddresses = getContractAddressesForChainOrThrow(1);
|
|
const quote: IndicativeQuote = {
|
|
maker: '0x64B92f5d9E5b5f20603de8498385c3a3d3048E22',
|
|
makerToken: contractAddresses.zrxToken,
|
|
makerAmount: new BigNumber(101),
|
|
takerToken: contractAddresses.etherToken,
|
|
takerAmount: new BigNumber(100),
|
|
expiry: NEVER_EXPIRES,
|
|
makerUri: MOCK_MM_URI,
|
|
};
|
|
|
|
const cacheClientMock = mock(CacheClient);
|
|
when(cacheClientMock.getNextOtcOrderBucketAsync(1337)).thenResolve(420);
|
|
when(cacheClientMock.getMakersInCooldownForPairAsync(anything(), anything(), anything())).thenResolve(
|
|
[],
|
|
);
|
|
|
|
// Mock out the dbUtils
|
|
const dbUtilsMock = mock(RfqmDbUtils);
|
|
when(dbUtilsMock.writeV2QuoteAsync(anything())).thenResolve();
|
|
const dbUtils = instance(dbUtilsMock);
|
|
|
|
const quoteServerClientMock = mock(QuoteServerClient);
|
|
when(quoteServerClientMock.batchGetPriceV2Async(anything(), anything(), anything())).thenResolve([
|
|
quote,
|
|
]);
|
|
const rfqMakerBalanceCacheServiceMock = mock(RfqMakerBalanceCacheService);
|
|
when(rfqMakerBalanceCacheServiceMock.getERC20OwnerBalancesAsync(anything(), anything())).thenResolve([
|
|
new BigNumber(150),
|
|
]);
|
|
|
|
const service = buildRfqmServiceForUnitTest({
|
|
quoteServerClient: instance(quoteServerClientMock),
|
|
dbUtils,
|
|
cacheClient: instance(cacheClientMock),
|
|
rfqMakerBalanceCacheService: instance(rfqMakerBalanceCacheServiceMock),
|
|
});
|
|
const approval: ApprovalResponse = {
|
|
isRequired: true,
|
|
isGaslessAvailable: true,
|
|
type: MOCK_EXECUTE_META_TRANSACTION_APPROVAL.kind,
|
|
eip712: MOCK_EXECUTE_META_TRANSACTION_APPROVAL.eip712,
|
|
};
|
|
const spiedService = spy(service);
|
|
when(spiedService.getGaslessApprovalResponseAsync(anything(), anything(), anything())).thenResolve(
|
|
approval,
|
|
);
|
|
|
|
const { quote: res } = await service.fetchFirmQuoteAsync({
|
|
integrator: MOCK_INTEGRATOR,
|
|
takerAddress,
|
|
buyToken: contractAddresses.zrxToken,
|
|
sellToken: contractAddresses.etherToken,
|
|
buyTokenDecimals: 18,
|
|
sellTokenDecimals: 18,
|
|
sellAmount,
|
|
checkApproval: true,
|
|
});
|
|
|
|
expect(res).to.exist;
|
|
expect(res?.type).to.equal(GaslessTypes.OtcOrder);
|
|
|
|
expect(res?.sellAmount).to.equal(sellAmount);
|
|
expect(res?.price.toNumber()).to.equal(1.01);
|
|
expect(res?.orderHash).to.match(/^0x[0-9a-fA-F]+/);
|
|
expect(Object.keys(res?.approval?.eip712?.domain ?? {})).to.eql([
|
|
'name',
|
|
'version',
|
|
'verifyingContract',
|
|
'salt',
|
|
]);
|
|
expect(res?.approval).to.eql(approval);
|
|
});
|
|
|
|
describe('Gasless RFQt VIP', () => {
|
|
it('should fetch a firm quote', async () => {
|
|
const sellAmount = new BigNumber(100);
|
|
const contractAddresses = getContractAddressesForChainOrThrow(1);
|
|
const quote: IndicativeQuote = {
|
|
maker: '0x64B92f5d9E5b5f20603de8498385c3a3d3048E22',
|
|
makerToken: contractAddresses.zrxToken,
|
|
makerAmount: new BigNumber(101),
|
|
takerToken: contractAddresses.etherToken,
|
|
takerAmount: new BigNumber(100),
|
|
expiry: NEVER_EXPIRES,
|
|
makerUri: MOCK_MM_URI,
|
|
};
|
|
const makerSignature = {
|
|
r: '',
|
|
s: '',
|
|
v: 28,
|
|
signatureType: SignatureType.EthSign,
|
|
};
|
|
|
|
const cacheClientMock = mock(CacheClient);
|
|
when(cacheClientMock.getNextOtcOrderBucketAsync(1337)).thenResolve(420);
|
|
when(
|
|
cacheClientMock.getMakersInCooldownForPairAsync(anything(), anything(), anything()),
|
|
).thenResolve([]);
|
|
|
|
// Mock out the dbUtils
|
|
const dbUtilsMock = mock(RfqmDbUtils);
|
|
when(dbUtilsMock.writeV2QuoteAsync(anything())).thenResolve();
|
|
const dbUtils = instance(dbUtilsMock);
|
|
|
|
const quoteServerClientMock = mock(QuoteServerClient);
|
|
when(
|
|
quoteServerClientMock.batchGetPriceV2Async(anything(), anything(), anything(), anything()),
|
|
).thenResolve([quote]);
|
|
when(
|
|
quoteServerClientMock.signV2Async(
|
|
quote.makerUri,
|
|
anything(),
|
|
anything(),
|
|
anything(),
|
|
anything(),
|
|
),
|
|
).thenResolve(makerSignature);
|
|
const rfqMakerBalanceCacheServiceMock = mock(RfqMakerBalanceCacheService);
|
|
when(
|
|
rfqMakerBalanceCacheServiceMock.getERC20OwnerBalancesAsync(anything(), anything()),
|
|
).thenResolve([new BigNumber(150)]);
|
|
|
|
const service = buildRfqmServiceForUnitTest({
|
|
quoteServerClient: instance(quoteServerClientMock),
|
|
dbUtils,
|
|
cacheClient: instance(cacheClientMock),
|
|
rfqMakerBalanceCacheService: instance(rfqMakerBalanceCacheServiceMock),
|
|
gaslessRfqtVipRolloutPercentage: 100,
|
|
});
|
|
|
|
// bypass smart contract wallet check
|
|
when(spy(SignatureUtils).getSignerFromHash(anything(), anything())).thenReturn(
|
|
'0x64B92f5d9E5b5f20603de8498385c3a3d3048E22',
|
|
);
|
|
|
|
const { quote: res } = await service.fetchFirmQuoteAsync({
|
|
integrator: MOCK_INTEGRATOR,
|
|
takerAddress,
|
|
buyToken: contractAddresses.zrxToken,
|
|
sellToken: contractAddresses.etherToken,
|
|
buyTokenDecimals: 18,
|
|
sellTokenDecimals: 18,
|
|
sellAmount,
|
|
checkApproval: false,
|
|
});
|
|
|
|
expect(res).to.exist;
|
|
expect(res?.type).to.equal(GaslessTypes.OtcOrder);
|
|
|
|
expect(res?.sellAmount).to.equal(sellAmount);
|
|
expect(res?.price.toNumber()).to.equal(1.01);
|
|
expect(res?.orderHash).to.match(/^0x[0-9a-fA-F]+/);
|
|
|
|
// verify that Gasless RFQt VIP specific params are written into the DB
|
|
const [quoteOpts] = capture(dbUtilsMock.writeV2QuoteAsync).last();
|
|
expect(quoteOpts.makerSignature).to.equal(makerSignature);
|
|
expect(quoteOpts.workflow).to.equal('gasless-rfqt');
|
|
});
|
|
|
|
it('should not throw if one of maker signatures cannot be fetched', async () => {
|
|
const sellAmount = new BigNumber(100);
|
|
const contractAddresses = getContractAddressesForChainOrThrow(1);
|
|
const validQuote: IndicativeQuote = {
|
|
maker: '0x64B92f5d9E5b5f20603de8498385c3a3d3048E22',
|
|
makerToken: contractAddresses.zrxToken,
|
|
makerAmount: new BigNumber(101),
|
|
takerToken: contractAddresses.etherToken,
|
|
takerAmount: new BigNumber(100),
|
|
expiry: NEVER_EXPIRES,
|
|
makerUri: MOCK_MM_URI,
|
|
};
|
|
const invalidQuote: IndicativeQuote = {
|
|
maker: '0x00000000000000000000000000000000DEADBEEF',
|
|
makerToken: contractAddresses.zrxToken,
|
|
makerAmount: new BigNumber(100),
|
|
takerToken: contractAddresses.etherToken,
|
|
takerAmount: new BigNumber(100),
|
|
expiry: NEVER_EXPIRES,
|
|
makerUri: 'https://bad-mm-address',
|
|
};
|
|
const makerSignature = {
|
|
r: '',
|
|
s: '',
|
|
v: 28,
|
|
signatureType: SignatureType.EthSign,
|
|
};
|
|
|
|
const cacheClientMock = mock(CacheClient);
|
|
when(cacheClientMock.getNextOtcOrderBucketAsync(1337)).thenResolve(420);
|
|
when(
|
|
cacheClientMock.getMakersInCooldownForPairAsync(anything(), anything(), anything()),
|
|
).thenResolve([]);
|
|
|
|
// Mock out the dbUtils
|
|
const dbUtilsMock = mock(RfqmDbUtils);
|
|
when(dbUtilsMock.writeV2QuoteAsync(anything())).thenResolve();
|
|
const dbUtils = instance(dbUtilsMock);
|
|
|
|
const quoteServerClientMock = mock(QuoteServerClient);
|
|
when(
|
|
quoteServerClientMock.batchGetPriceV2Async(anything(), anything(), anything(), anything()),
|
|
).thenResolve([validQuote, invalidQuote]);
|
|
when(
|
|
quoteServerClientMock.signV2Async(
|
|
validQuote.makerUri,
|
|
anything(),
|
|
anything(),
|
|
anything(),
|
|
anything(),
|
|
),
|
|
).thenResolve(makerSignature);
|
|
when(
|
|
quoteServerClientMock.signV2Async(
|
|
invalidQuote.makerUri,
|
|
anything(),
|
|
anything(),
|
|
anything(),
|
|
anything(),
|
|
),
|
|
).thenThrow();
|
|
const rfqMakerBalanceCacheServiceMock = mock(RfqMakerBalanceCacheService);
|
|
when(
|
|
rfqMakerBalanceCacheServiceMock.getERC20OwnerBalancesAsync(anything(), anything()),
|
|
).thenResolve([new BigNumber(150), new BigNumber(100)]);
|
|
|
|
const service = buildRfqmServiceForUnitTest({
|
|
quoteServerClient: instance(quoteServerClientMock),
|
|
dbUtils,
|
|
cacheClient: instance(cacheClientMock),
|
|
rfqMakerBalanceCacheService: instance(rfqMakerBalanceCacheServiceMock),
|
|
gaslessRfqtVipRolloutPercentage: 100,
|
|
});
|
|
|
|
// bypass smart contract wallet check
|
|
when(spy(SignatureUtils).getSignerFromHash(anything(), anything())).thenReturn(
|
|
'0x64B92f5d9E5b5f20603de8498385c3a3d3048E22',
|
|
);
|
|
|
|
const { quote: res } = await service.fetchFirmQuoteAsync({
|
|
integrator: MOCK_INTEGRATOR,
|
|
takerAddress,
|
|
buyToken: contractAddresses.zrxToken,
|
|
sellToken: contractAddresses.etherToken,
|
|
buyTokenDecimals: 18,
|
|
sellTokenDecimals: 18,
|
|
sellAmount,
|
|
checkApproval: false,
|
|
});
|
|
|
|
expect(res).to.exist;
|
|
expect(res?.type).to.equal(GaslessTypes.OtcOrder);
|
|
|
|
expect(res?.sellAmount).to.equal(sellAmount);
|
|
expect(res?.price.toNumber()).to.equal(1.01);
|
|
expect(res?.orderHash).to.match(/^0x[0-9a-fA-F]+/);
|
|
|
|
// verify that Gasless RFQt VIP specific params are written into the DB
|
|
const [quoteOpts] = capture(dbUtilsMock.writeV2QuoteAsync).last();
|
|
expect(quoteOpts.makerSignature).to.equal(makerSignature);
|
|
expect(quoteOpts.workflow).to.equal('gasless-rfqt');
|
|
});
|
|
|
|
it('should not throw if both maker signatures cannot be fetched', async () => {
|
|
const sellAmount = new BigNumber(100);
|
|
const contractAddresses = getContractAddressesForChainOrThrow(1);
|
|
const invalidQuote1: IndicativeQuote = {
|
|
maker: '0x00000000000000000000000000000000DEADBEEF',
|
|
makerToken: contractAddresses.zrxToken,
|
|
makerAmount: new BigNumber(101),
|
|
takerToken: contractAddresses.etherToken,
|
|
takerAmount: new BigNumber(100),
|
|
expiry: NEVER_EXPIRES,
|
|
makerUri: MOCK_MM_URI,
|
|
};
|
|
const invalidQuote2: IndicativeQuote = {
|
|
maker: '0x00000000000000000000000000000000FEEBDAED',
|
|
makerToken: contractAddresses.zrxToken,
|
|
makerAmount: new BigNumber(100),
|
|
takerToken: contractAddresses.etherToken,
|
|
takerAmount: new BigNumber(100),
|
|
expiry: NEVER_EXPIRES,
|
|
makerUri: 'https://bad-mm-address',
|
|
};
|
|
|
|
const cacheClientMock = mock(CacheClient);
|
|
when(cacheClientMock.getNextOtcOrderBucketAsync(1337)).thenResolve(420);
|
|
when(
|
|
cacheClientMock.getMakersInCooldownForPairAsync(anything(), anything(), anything()),
|
|
).thenResolve([]);
|
|
|
|
// Mock out the dbUtils
|
|
const dbUtilsMock = mock(RfqmDbUtils);
|
|
when(dbUtilsMock.writeV2QuoteAsync(anything())).thenResolve();
|
|
const dbUtils = instance(dbUtilsMock);
|
|
|
|
const quoteServerClientMock = mock(QuoteServerClient);
|
|
when(
|
|
quoteServerClientMock.batchGetPriceV2Async(anything(), anything(), anything(), anything()),
|
|
).thenResolve([invalidQuote1, invalidQuote2]);
|
|
when(
|
|
quoteServerClientMock.signV2Async(anything(), anything(), anything(), anything(), anything()),
|
|
).thenThrow();
|
|
const rfqMakerBalanceCacheServiceMock = mock(RfqMakerBalanceCacheService);
|
|
when(
|
|
rfqMakerBalanceCacheServiceMock.getERC20OwnerBalancesAsync(anything(), anything()),
|
|
).thenResolve([new BigNumber(150), new BigNumber(100)]);
|
|
|
|
const service = buildRfqmServiceForUnitTest({
|
|
quoteServerClient: instance(quoteServerClientMock),
|
|
dbUtils,
|
|
cacheClient: instance(cacheClientMock),
|
|
rfqMakerBalanceCacheService: instance(rfqMakerBalanceCacheServiceMock),
|
|
gaslessRfqtVipRolloutPercentage: 100,
|
|
});
|
|
|
|
const { quote: res } = await service.fetchFirmQuoteAsync({
|
|
integrator: MOCK_INTEGRATOR,
|
|
takerAddress,
|
|
buyToken: contractAddresses.zrxToken,
|
|
sellToken: contractAddresses.etherToken,
|
|
buyTokenDecimals: 18,
|
|
sellTokenDecimals: 18,
|
|
sellAmount,
|
|
checkApproval: false,
|
|
});
|
|
|
|
expect(res).to.be.null;
|
|
});
|
|
|
|
it('should only fetch RFQm quote if checkApproval is true', async () => {
|
|
const sellAmount = new BigNumber(100);
|
|
const contractAddresses = getContractAddressesForChainOrThrow(1);
|
|
const quote: IndicativeQuote = {
|
|
maker: '0x64B92f5d9E5b5f20603de8498385c3a3d3048E22',
|
|
makerToken: contractAddresses.zrxToken,
|
|
makerAmount: new BigNumber(101),
|
|
takerToken: contractAddresses.etherToken,
|
|
takerAmount: new BigNumber(100),
|
|
expiry: NEVER_EXPIRES,
|
|
makerUri: MOCK_MM_URI,
|
|
};
|
|
|
|
const cacheClientMock = mock(CacheClient);
|
|
when(cacheClientMock.getNextOtcOrderBucketAsync(1337)).thenResolve(420);
|
|
when(
|
|
cacheClientMock.getMakersInCooldownForPairAsync(anything(), anything(), anything()),
|
|
).thenResolve([]);
|
|
|
|
// Mock out the dbUtils
|
|
const dbUtilsMock = mock(RfqmDbUtils);
|
|
when(dbUtilsMock.writeV2QuoteAsync(anything())).thenResolve();
|
|
const dbUtils = instance(dbUtilsMock);
|
|
|
|
const quoteServerClientMock = mock(QuoteServerClient);
|
|
when(quoteServerClientMock.batchGetPriceV2Async(anything(), anything(), anything())).thenResolve([
|
|
quote,
|
|
]);
|
|
const rfqMakerBalanceCacheServiceMock = mock(RfqMakerBalanceCacheService);
|
|
when(
|
|
rfqMakerBalanceCacheServiceMock.getERC20OwnerBalancesAsync(anything(), anything()),
|
|
).thenResolve([new BigNumber(150)]);
|
|
|
|
const service = buildRfqmServiceForUnitTest({
|
|
quoteServerClient: instance(quoteServerClientMock),
|
|
dbUtils,
|
|
cacheClient: instance(cacheClientMock),
|
|
rfqMakerBalanceCacheService: instance(rfqMakerBalanceCacheServiceMock),
|
|
gaslessRfqtVipRolloutPercentage: 100,
|
|
});
|
|
const approval: ApprovalResponse = {
|
|
isRequired: true,
|
|
isGaslessAvailable: true,
|
|
type: MOCK_EXECUTE_META_TRANSACTION_APPROVAL.kind,
|
|
eip712: MOCK_EXECUTE_META_TRANSACTION_APPROVAL.eip712,
|
|
};
|
|
const spiedService = spy(service);
|
|
when(spiedService.getGaslessApprovalResponseAsync(anything(), anything(), anything())).thenResolve(
|
|
approval,
|
|
);
|
|
|
|
const { quote: res } = await service.fetchFirmQuoteAsync({
|
|
integrator: MOCK_INTEGRATOR,
|
|
takerAddress,
|
|
buyToken: contractAddresses.zrxToken,
|
|
sellToken: contractAddresses.etherToken,
|
|
buyTokenDecimals: 18,
|
|
sellTokenDecimals: 18,
|
|
sellAmount,
|
|
checkApproval: true,
|
|
});
|
|
|
|
expect(res).to.exist;
|
|
expect(res?.type).to.equal(GaslessTypes.OtcOrder);
|
|
|
|
expect(res?.sellAmount).to.equal(sellAmount);
|
|
expect(res?.price.toNumber()).to.equal(1.01);
|
|
expect(res?.orderHash).to.match(/^0x[0-9a-fA-F]+/);
|
|
expect(Object.keys(res?.approval?.eip712?.domain ?? {})).to.eql([
|
|
'name',
|
|
'version',
|
|
'verifyingContract',
|
|
'salt',
|
|
]);
|
|
expect(res?.approval).to.eql(approval);
|
|
|
|
// verify that the workflow is `rfqm`
|
|
const [quoteOpts] = capture(dbUtilsMock.writeV2QuoteAsync).last();
|
|
expect(quoteOpts.makerSignature).to.not.exist;
|
|
expect(quoteOpts.workflow).to.equal('rfqm');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('buys', () => {
|
|
it('should fetch a firm quote', async () => {
|
|
const buyAmount = new BigNumber(100);
|
|
const contractAddresses = getContractAddressesForChainOrThrow(1);
|
|
const quote: IndicativeQuote = {
|
|
maker: '0x64B92f5d9E5b5f20603de8498385c3a3d3048E22',
|
|
makerToken: contractAddresses.zrxToken,
|
|
makerAmount: new BigNumber(100),
|
|
takerToken: contractAddresses.etherToken,
|
|
takerAmount: new BigNumber(80),
|
|
expiry: NEVER_EXPIRES,
|
|
makerUri: MOCK_MM_URI,
|
|
};
|
|
|
|
const cacheClientMock = mock(CacheClient);
|
|
when(cacheClientMock.getNextOtcOrderBucketAsync(1337)).thenResolve(420);
|
|
when(cacheClientMock.getMakersInCooldownForPairAsync(anything(), anything(), anything())).thenResolve(
|
|
[],
|
|
);
|
|
|
|
// Mock out the dbUtils
|
|
const dbUtilsMock = mock(RfqmDbUtils);
|
|
when(dbUtilsMock.writeV2QuoteAsync(anything())).thenResolve();
|
|
const dbUtils = instance(dbUtilsMock);
|
|
|
|
const quoteServerClientMock = mock(QuoteServerClient);
|
|
when(quoteServerClientMock.batchGetPriceV2Async(anything(), anything(), anything())).thenResolve([
|
|
quote,
|
|
]);
|
|
const rfqMakerBalanceCacheServiceMock = mock(RfqMakerBalanceCacheService);
|
|
when(rfqMakerBalanceCacheServiceMock.getERC20OwnerBalancesAsync(anything(), anything())).thenResolve([
|
|
new BigNumber(150),
|
|
]);
|
|
|
|
const service = buildRfqmServiceForUnitTest({
|
|
quoteServerClient: instance(quoteServerClientMock),
|
|
dbUtils,
|
|
cacheClient: instance(cacheClientMock),
|
|
rfqMakerBalanceCacheService: instance(rfqMakerBalanceCacheServiceMock),
|
|
});
|
|
|
|
const { quote: res } = await service.fetchFirmQuoteAsync({
|
|
integrator: MOCK_INTEGRATOR,
|
|
takerAddress,
|
|
buyToken: contractAddresses.zrxToken,
|
|
sellToken: contractAddresses.etherToken,
|
|
buyTokenDecimals: 18,
|
|
sellTokenDecimals: 18,
|
|
buyAmount: new BigNumber(100),
|
|
checkApproval: false,
|
|
});
|
|
|
|
expect(res).to.exist;
|
|
expect(res?.type).to.equal(GaslessTypes.OtcOrder);
|
|
expect(res?.buyAmount.toNumber()).to.equal(buyAmount.toNumber());
|
|
expect(res?.price.toNumber()).to.equal(0.8);
|
|
expect(res?.orderHash).to.match(/^0x[0-9a-fA-F]+/);
|
|
});
|
|
|
|
// TODO: we may want to reintroduce this test very soon. However, if not addressed by June 2022, remove
|
|
it.skip('should scale a firm quote to desired buyAmount if MM returns too much', async () => {
|
|
const buyAmount = new BigNumber(100);
|
|
const contractAddresses = getContractAddressesForChainOrThrow(1);
|
|
const quote: IndicativeQuote = {
|
|
maker: '0x64B92f5d9E5b5f20603de8498385c3a3d3048E22',
|
|
makerToken: contractAddresses.zrxToken,
|
|
makerAmount: new BigNumber(125), // more than buyAmount
|
|
takerToken: contractAddresses.etherToken,
|
|
takerAmount: new BigNumber(100),
|
|
expiry: NEVER_EXPIRES,
|
|
makerUri: MOCK_MM_URI,
|
|
};
|
|
|
|
const cacheClientMock = mock(CacheClient);
|
|
when(cacheClientMock.getNextOtcOrderBucketAsync(1337)).thenResolve(420);
|
|
when(cacheClientMock.getMakersInCooldownForPairAsync(anything(), anything(), anything())).thenResolve(
|
|
[],
|
|
);
|
|
|
|
// Mock out the dbUtils
|
|
const dbUtilsMock = mock(RfqmDbUtils);
|
|
when(dbUtilsMock.writeV2QuoteAsync(anything())).thenResolve();
|
|
const dbUtils = instance(dbUtilsMock);
|
|
|
|
const quoteServerClientMock = mock(QuoteServerClient);
|
|
when(quoteServerClientMock.batchGetPriceV2Async(anything(), anything(), anything())).thenResolve([
|
|
quote,
|
|
]);
|
|
const rfqMakerBalanceCacheServiceMock = mock(RfqMakerBalanceCacheService);
|
|
when(rfqMakerBalanceCacheServiceMock.getERC20OwnerBalancesAsync(anything(), anything())).thenResolve([
|
|
new BigNumber(150),
|
|
]);
|
|
|
|
const service = buildRfqmServiceForUnitTest({
|
|
quoteServerClient: instance(quoteServerClientMock),
|
|
dbUtils,
|
|
cacheClient: instance(cacheClientMock),
|
|
rfqMakerBalanceCacheService: instance(rfqMakerBalanceCacheServiceMock),
|
|
});
|
|
|
|
const { quote: res } = await service.fetchFirmQuoteAsync({
|
|
integrator: MOCK_INTEGRATOR,
|
|
takerAddress,
|
|
buyToken: contractAddresses.zrxToken,
|
|
sellToken: contractAddresses.etherToken,
|
|
buyTokenDecimals: 18,
|
|
sellTokenDecimals: 18,
|
|
buyAmount: new BigNumber(100),
|
|
checkApproval: false,
|
|
});
|
|
|
|
expect(res).to.exist;
|
|
expect(res?.type).to.equal(GaslessTypes.OtcOrder);
|
|
expect(res?.buyAmount.toNumber()).to.equal(buyAmount.toNumber());
|
|
expect(res?.sellAmount.toNumber()).to.equal(80); // result is scaled
|
|
expect(res?.price.toNumber()).to.equal(0.8);
|
|
expect(res?.orderHash).to.match(/^0x[0-9a-fA-F]+/);
|
|
});
|
|
|
|
describe('Gasless RFQt VIP', () => {
|
|
it('should fetch a firm quote', async () => {
|
|
const buyAmount = new BigNumber(100);
|
|
const contractAddresses = getContractAddressesForChainOrThrow(1);
|
|
const quote: IndicativeQuote = {
|
|
maker: '0x64B92f5d9E5b5f20603de8498385c3a3d3048E22',
|
|
makerToken: contractAddresses.zrxToken,
|
|
makerAmount: new BigNumber(100),
|
|
takerToken: contractAddresses.etherToken,
|
|
takerAmount: new BigNumber(80),
|
|
expiry: NEVER_EXPIRES,
|
|
makerUri: MOCK_MM_URI,
|
|
};
|
|
const makerSignature = {
|
|
r: '',
|
|
s: '',
|
|
v: 28,
|
|
signatureType: SignatureType.EthSign,
|
|
};
|
|
|
|
const cacheClientMock = mock(CacheClient);
|
|
when(cacheClientMock.getNextOtcOrderBucketAsync(1337)).thenResolve(420);
|
|
when(
|
|
cacheClientMock.getMakersInCooldownForPairAsync(anything(), anything(), anything()),
|
|
).thenResolve([]);
|
|
|
|
// Mock out the dbUtils
|
|
const dbUtilsMock = mock(RfqmDbUtils);
|
|
when(dbUtilsMock.writeV2QuoteAsync(anything())).thenResolve();
|
|
const dbUtils = instance(dbUtilsMock);
|
|
|
|
const quoteServerClientMock = mock(QuoteServerClient);
|
|
when(
|
|
quoteServerClientMock.batchGetPriceV2Async(anything(), anything(), anything(), anything()),
|
|
).thenResolve([quote]);
|
|
when(
|
|
quoteServerClientMock.signV2Async(
|
|
quote.makerUri,
|
|
anything(),
|
|
anything(),
|
|
anything(),
|
|
anything(),
|
|
),
|
|
).thenResolve(makerSignature);
|
|
|
|
const rfqMakerBalanceCacheServiceMock = mock(RfqMakerBalanceCacheService);
|
|
when(
|
|
rfqMakerBalanceCacheServiceMock.getERC20OwnerBalancesAsync(anything(), anything()),
|
|
).thenResolve([new BigNumber(150)]);
|
|
|
|
const service = buildRfqmServiceForUnitTest({
|
|
quoteServerClient: instance(quoteServerClientMock),
|
|
dbUtils,
|
|
cacheClient: instance(cacheClientMock),
|
|
rfqMakerBalanceCacheService: instance(rfqMakerBalanceCacheServiceMock),
|
|
gaslessRfqtVipRolloutPercentage: 100,
|
|
});
|
|
|
|
// bypass smart contract wallet check
|
|
when(spy(SignatureUtils).getSignerFromHash(anything(), anything())).thenReturn(
|
|
'0x64B92f5d9E5b5f20603de8498385c3a3d3048E22',
|
|
);
|
|
|
|
const { quote: res } = await service.fetchFirmQuoteAsync({
|
|
integrator: MOCK_INTEGRATOR,
|
|
takerAddress,
|
|
buyToken: contractAddresses.zrxToken,
|
|
sellToken: contractAddresses.etherToken,
|
|
buyTokenDecimals: 18,
|
|
sellTokenDecimals: 18,
|
|
buyAmount: new BigNumber(100),
|
|
checkApproval: false,
|
|
});
|
|
|
|
expect(res).to.exist;
|
|
expect(res?.type).to.equal(GaslessTypes.OtcOrder);
|
|
expect(res?.buyAmount.toNumber()).to.equal(buyAmount.toNumber());
|
|
expect(res?.price.toNumber()).to.equal(0.8);
|
|
expect(res?.orderHash).to.match(/^0x[0-9a-fA-F]+/);
|
|
|
|
// verify that Gasless RFQt VIP specific params are written into the DB
|
|
const [quoteOpts] = capture(dbUtilsMock.writeV2QuoteAsync).last();
|
|
expect(quoteOpts.makerSignature).to.equal(makerSignature);
|
|
expect(quoteOpts.workflow).to.equal('gasless-rfqt');
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('getGaslessApprovalResponseAsync', () => {
|
|
it('returns correct approval field', async () => {
|
|
const service = buildRfqmServiceForUnitTest({ chainId: 137 });
|
|
|
|
let approval = await service.getGaslessApprovalResponseAsync(
|
|
WORKER_TEST_ADDRESS,
|
|
MOCK_TOKEN,
|
|
new BigNumber(100),
|
|
);
|
|
expect(approval).to.eql({ isRequired: false });
|
|
|
|
approval = await service.getGaslessApprovalResponseAsync(
|
|
WORKER_TEST_ADDRESS,
|
|
'0x123456',
|
|
new BigNumber(100),
|
|
);
|
|
expect(approval).to.eql({ isRequired: true, isGaslessAvailable: false });
|
|
|
|
approval = await service.getGaslessApprovalResponseAsync(
|
|
WORKER_TEST_ADDRESS,
|
|
MOCK_TOKEN,
|
|
new BigNumber(100),
|
|
);
|
|
expect(approval).to.eql({
|
|
isRequired: true,
|
|
isGaslessAvailable: true,
|
|
type: MOCK_EXECUTE_META_TRANSACTION_APPROVAL.kind,
|
|
hash: MOCK_EXECUTE_META_TRANSACTION_HASH,
|
|
eip712: MOCK_EXECUTE_META_TRANSACTION_APPROVAL.eip712,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('runHealthCheckAsync', () => {
|
|
it('returns active pairs', async () => {
|
|
const dbUtilsMock = mock(RfqmDbUtils);
|
|
when(dbUtilsMock.findRfqmWorkerHeartbeatsAsync(1337)).thenResolve([]);
|
|
|
|
const rfqMakerManagerMock = mock(RfqMakerManager);
|
|
when(rfqMakerManagerMock.getRfqmV2MakerOfferings()).thenReturn({
|
|
'https://mock-rfqm1.club': [
|
|
['0x871dd7c2b4b25e1aa18728e9d5f2af4c4e431f5c', '0x0b1ba0af832d7c05fd64161e0db78e85978e8082'],
|
|
],
|
|
});
|
|
|
|
const service = buildRfqmServiceForUnitTest({
|
|
dbUtils: instance(dbUtilsMock),
|
|
rfqMakerManager: instance(rfqMakerManagerMock),
|
|
});
|
|
|
|
const result = await service.runHealthCheckAsync();
|
|
|
|
expect(result.pairs).to.have.key(
|
|
'0x0b1ba0af832d7c05fd64161e0db78e85978e8082-0x871dd7c2b4b25e1aa18728e9d5f2af4c4e431f5c',
|
|
);
|
|
expect(
|
|
result.pairs['0x0b1ba0af832d7c05fd64161e0db78e85978e8082-0x871dd7c2b4b25e1aa18728e9d5f2af4c4e431f5c'],
|
|
).to.equal(HealthCheckStatus.Operational);
|
|
});
|
|
});
|
|
|
|
describe('status', () => {
|
|
describe('v2', () => {
|
|
const expiry = new BigNumber(Date.now() + 1_000_000).dividedBy(ONE_SECOND_MS).decimalPlaces(0);
|
|
const chainId = 1337;
|
|
const otcOrder = new OtcOrder({
|
|
txOrigin: '0x0000000000000000000000000000000000000000',
|
|
taker: '0x1111111111111111111111111111111111111111',
|
|
maker: '0x2222222222222222222222222222222222222222',
|
|
makerToken: '0x3333333333333333333333333333333333333333',
|
|
takerToken: '0x4444444444444444444444444444444444444444',
|
|
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(expiry, ZERO, expiry),
|
|
chainId,
|
|
verifyingContract: '0x0000000000000000000000000000000000000000',
|
|
});
|
|
const BASE_JOB = new RfqmV2JobEntity({
|
|
chainId,
|
|
expiry,
|
|
makerUri: '',
|
|
orderHash: '0x00',
|
|
fee: {
|
|
token: '0xToken',
|
|
amount: '100',
|
|
type: 'fixed',
|
|
},
|
|
order: otcOrderToStoredOtcOrder(otcOrder),
|
|
workflow: 'rfqm',
|
|
});
|
|
it('should return failed for jobs that have sat in queue past expiry', async () => {
|
|
const expired = new BigNumber(Date.now() - 10000).dividedBy(ONE_SECOND_MS).decimalPlaces(0);
|
|
const oldJob = new RfqmV2JobEntity({ ...BASE_JOB, expiry: expired });
|
|
const dbUtilsMock = mock(RfqmDbUtils);
|
|
when(dbUtilsMock.findV2JobByOrderHashAsync(anything())).thenResolve(oldJob);
|
|
const service = buildRfqmServiceForUnitTest({ dbUtils: instance(dbUtilsMock) });
|
|
|
|
const jobStatus = await service.getStatusAsync('0x00');
|
|
|
|
if (jobStatus === null) {
|
|
expect.fail('Status should exist');
|
|
throw new Error();
|
|
}
|
|
expect(jobStatus.status).to.equal('failed');
|
|
|
|
if (jobStatus.status !== 'failed') {
|
|
expect.fail('Status should be failed');
|
|
throw new Error();
|
|
}
|
|
expect(jobStatus.transactions).to.have.length(0);
|
|
});
|
|
|
|
it('should return pending for unexpired enqueued jobs', async () => {
|
|
const newJob = BASE_JOB; // BASE_JOB has a valid expiry
|
|
const dbUtilsMock = mock(RfqmDbUtils);
|
|
when(dbUtilsMock.findV2JobByOrderHashAsync(anything())).thenResolve(newJob);
|
|
when(dbUtilsMock.findV2TransactionSubmissionsByOrderHashAsync(anything(), anything())).thenResolve([]);
|
|
const service = buildRfqmServiceForUnitTest({ dbUtils: instance(dbUtilsMock) });
|
|
|
|
const jobStatus = await service.getStatusAsync('0x00');
|
|
|
|
if (jobStatus === null) {
|
|
expect.fail('Status should exist');
|
|
throw new Error();
|
|
}
|
|
expect(jobStatus.status).to.equal('pending');
|
|
});
|
|
|
|
it('should return pending for jobs in processing', async () => {
|
|
const job = new RfqmV2JobEntity({ ...BASE_JOB, status: RfqmJobStatus.PendingProcessing });
|
|
const dbUtilsMock = mock(RfqmDbUtils);
|
|
when(dbUtilsMock.findV2JobByOrderHashAsync(anything())).thenResolve(job);
|
|
when(dbUtilsMock.findV2TransactionSubmissionsByOrderHashAsync(anything(), anything())).thenResolve([]);
|
|
const service = buildRfqmServiceForUnitTest({ dbUtils: instance(dbUtilsMock) });
|
|
|
|
const jobStatus = await service.getStatusAsync('0x00');
|
|
|
|
if (jobStatus === null) {
|
|
expect.fail('Status should exist');
|
|
throw new Error();
|
|
}
|
|
expect(jobStatus.status).to.equal('pending');
|
|
});
|
|
|
|
it('should return submitted with transaction submissions for submitted jobs', async () => {
|
|
const now = Date.now();
|
|
const transaction1Time = now + 10;
|
|
const transaction2Time = now + 20;
|
|
|
|
const job = new RfqmV2JobEntity({
|
|
...BASE_JOB,
|
|
status: RfqmJobStatus.PendingSubmitted,
|
|
});
|
|
|
|
const submission1 = new RfqmV2TransactionSubmissionEntity({
|
|
createdAt: new Date(transaction1Time),
|
|
orderHash: job.orderHash,
|
|
transactionHash: '0x01',
|
|
from: job.order.order.txOrigin,
|
|
to: job.order.order.verifyingContract,
|
|
type: RfqmTransactionSubmissionType.Trade,
|
|
nonce: 0,
|
|
});
|
|
const submission2 = new RfqmV2TransactionSubmissionEntity({
|
|
createdAt: new Date(transaction2Time),
|
|
orderHash: job.orderHash,
|
|
transactionHash: '0x02',
|
|
from: job.order.order.txOrigin,
|
|
to: job.order.order.verifyingContract,
|
|
type: RfqmTransactionSubmissionType.Trade,
|
|
nonce: 1,
|
|
});
|
|
|
|
const dbUtilsMock = mock(RfqmDbUtils);
|
|
when(dbUtilsMock.findV2JobByOrderHashAsync(anything())).thenResolve(job);
|
|
when(
|
|
dbUtilsMock.findV2TransactionSubmissionsByOrderHashAsync(
|
|
job.orderHash,
|
|
deepEqual([
|
|
RfqmTransactionSubmissionType.Trade,
|
|
RfqmTransactionSubmissionType.ApprovalAndTrade,
|
|
]),
|
|
),
|
|
).thenResolve([submission1, submission2]);
|
|
const service = buildRfqmServiceForUnitTest({ dbUtils: instance(dbUtilsMock) });
|
|
|
|
const jobStatus = await service.getStatusAsync('0x00');
|
|
|
|
if (jobStatus === null) {
|
|
expect.fail('Status should exist');
|
|
throw new Error();
|
|
}
|
|
|
|
if (jobStatus.status !== 'submitted') {
|
|
expect.fail('Status should be submitted');
|
|
throw new Error();
|
|
}
|
|
expect(jobStatus.transactions).to.have.length(2);
|
|
expect(jobStatus.transactions).to.deep.include({
|
|
hash: '0x01',
|
|
timestamp: +transaction1Time.valueOf(),
|
|
});
|
|
expect(jobStatus.transactions).to.deep.include({
|
|
hash: '0x02',
|
|
timestamp: +transaction2Time.valueOf(),
|
|
});
|
|
});
|
|
|
|
it('[ApprovalAndTrade] should return submitted with transaction submissions for submitted jobs', async () => {
|
|
const now = Date.now();
|
|
const transaction1Time = now + 10;
|
|
const transaction2Time = now + 20;
|
|
|
|
const job = new RfqmV2JobEntity({
|
|
...BASE_JOB,
|
|
status: RfqmJobStatus.PendingSubmitted,
|
|
});
|
|
|
|
const submission1 = new RfqmV2TransactionSubmissionEntity({
|
|
createdAt: new Date(transaction1Time),
|
|
orderHash: job.orderHash,
|
|
transactionHash: '0x01',
|
|
from: job.order.order.txOrigin,
|
|
to: job.order.order.verifyingContract,
|
|
type: RfqmTransactionSubmissionType.ApprovalAndTrade,
|
|
nonce: 0,
|
|
});
|
|
const submission2 = new RfqmV2TransactionSubmissionEntity({
|
|
createdAt: new Date(transaction2Time),
|
|
orderHash: job.orderHash,
|
|
transactionHash: '0x02',
|
|
from: job.order.order.txOrigin,
|
|
to: job.order.order.verifyingContract,
|
|
type: RfqmTransactionSubmissionType.ApprovalAndTrade,
|
|
nonce: 1,
|
|
});
|
|
|
|
const dbUtilsMock = mock(RfqmDbUtils);
|
|
when(dbUtilsMock.findV2JobByOrderHashAsync(anything())).thenResolve(job);
|
|
when(
|
|
dbUtilsMock.findV2TransactionSubmissionsByOrderHashAsync(
|
|
job.orderHash,
|
|
deepEqual([
|
|
RfqmTransactionSubmissionType.Trade,
|
|
RfqmTransactionSubmissionType.ApprovalAndTrade,
|
|
]),
|
|
),
|
|
).thenResolve([submission1, submission2]);
|
|
const service = buildRfqmServiceForUnitTest({ dbUtils: instance(dbUtilsMock) });
|
|
|
|
const jobStatus = await service.getStatusAsync('0x00');
|
|
|
|
if (jobStatus === null) {
|
|
expect.fail('Status should exist');
|
|
throw new Error();
|
|
}
|
|
|
|
if (jobStatus.status !== 'submitted') {
|
|
expect.fail('Status should be submitted');
|
|
throw new Error();
|
|
}
|
|
expect(jobStatus.transactions).to.have.length(2);
|
|
expect(jobStatus.transactions).to.deep.include({
|
|
hash: '0x01',
|
|
timestamp: +transaction1Time.valueOf(),
|
|
});
|
|
expect(jobStatus.transactions).to.deep.include({
|
|
hash: '0x02',
|
|
timestamp: +transaction2Time.valueOf(),
|
|
});
|
|
});
|
|
|
|
it('should return succeeded for a successful job, with the succeeded job', async () => {
|
|
const now = Date.now();
|
|
const transaction1Time = now + 10;
|
|
const transaction2Time = now + 20;
|
|
|
|
const job = new RfqmV2JobEntity({
|
|
...BASE_JOB,
|
|
status: RfqmJobStatus.SucceededUnconfirmed,
|
|
});
|
|
|
|
const submission1 = new RfqmV2TransactionSubmissionEntity({
|
|
createdAt: new Date(transaction1Time),
|
|
orderHash: job.orderHash,
|
|
transactionHash: '0x01',
|
|
status: RfqmTransactionSubmissionStatus.DroppedAndReplaced,
|
|
from: job.order.order.txOrigin,
|
|
to: job.order.order.verifyingContract,
|
|
type: RfqmTransactionSubmissionType.Trade,
|
|
nonce: 0,
|
|
});
|
|
const submission2 = new RfqmV2TransactionSubmissionEntity({
|
|
createdAt: new Date(transaction2Time),
|
|
orderHash: job.orderHash,
|
|
transactionHash: '0x02',
|
|
status: RfqmTransactionSubmissionStatus.SucceededUnconfirmed,
|
|
from: job.order.order.txOrigin,
|
|
to: job.order.order.verifyingContract,
|
|
type: RfqmTransactionSubmissionType.Trade,
|
|
nonce: 1,
|
|
});
|
|
|
|
const dbUtilsMock = mock(RfqmDbUtils);
|
|
when(dbUtilsMock.findV2JobByOrderHashAsync(anything())).thenResolve(job);
|
|
when(
|
|
dbUtilsMock.findV2TransactionSubmissionsByOrderHashAsync(
|
|
job.orderHash,
|
|
deepEqual([
|
|
RfqmTransactionSubmissionType.Trade,
|
|
RfqmTransactionSubmissionType.ApprovalAndTrade,
|
|
]),
|
|
),
|
|
).thenResolve([submission1, submission2]);
|
|
const service = buildRfqmServiceForUnitTest({ dbUtils: instance(dbUtilsMock) });
|
|
|
|
const jobStatus = await service.getStatusAsync('0x00');
|
|
|
|
if (jobStatus === null) {
|
|
expect.fail('Status should exist');
|
|
throw new Error();
|
|
}
|
|
|
|
if (jobStatus.status !== 'succeeded') {
|
|
expect.fail('Status should be succeeded');
|
|
throw new Error();
|
|
}
|
|
expect(jobStatus.transactions[0]).to.contain({ hash: '0x02', timestamp: +transaction2Time.valueOf() });
|
|
});
|
|
|
|
it('should return confirmed for a successful confirmed job', async () => {
|
|
const now = Date.now();
|
|
const transaction1Time = now + 10;
|
|
const transaction2Time = now + 20;
|
|
|
|
const job = new RfqmV2JobEntity({
|
|
...BASE_JOB,
|
|
status: RfqmJobStatus.SucceededConfirmed,
|
|
});
|
|
|
|
const submission1 = new RfqmV2TransactionSubmissionEntity({
|
|
createdAt: new Date(transaction1Time),
|
|
orderHash: job.orderHash,
|
|
transactionHash: '0x01',
|
|
status: RfqmTransactionSubmissionStatus.DroppedAndReplaced,
|
|
from: job.order.order.txOrigin,
|
|
to: job.order.order.verifyingContract,
|
|
type: RfqmTransactionSubmissionType.Trade,
|
|
nonce: 0,
|
|
});
|
|
const submission2 = new RfqmV2TransactionSubmissionEntity({
|
|
createdAt: new Date(transaction2Time),
|
|
orderHash: job.orderHash,
|
|
transactionHash: '0x02',
|
|
status: RfqmTransactionSubmissionStatus.SucceededConfirmed,
|
|
from: job.order.order.txOrigin,
|
|
to: job.order.order.verifyingContract,
|
|
type: RfqmTransactionSubmissionType.Trade,
|
|
nonce: 1,
|
|
});
|
|
|
|
const dbUtilsMock = mock(RfqmDbUtils);
|
|
when(dbUtilsMock.findV2JobByOrderHashAsync(anything())).thenResolve(job);
|
|
when(
|
|
dbUtilsMock.findV2TransactionSubmissionsByOrderHashAsync(
|
|
job.orderHash,
|
|
deepEqual([
|
|
RfqmTransactionSubmissionType.Trade,
|
|
RfqmTransactionSubmissionType.ApprovalAndTrade,
|
|
]),
|
|
),
|
|
).thenResolve([submission1, submission2]);
|
|
const service = buildRfqmServiceForUnitTest({ dbUtils: instance(dbUtilsMock) });
|
|
|
|
const jobStatus = await service.getStatusAsync('0x00');
|
|
|
|
if (jobStatus === null) {
|
|
expect.fail('Status should exist');
|
|
throw new Error();
|
|
}
|
|
|
|
if (jobStatus.status !== 'confirmed') {
|
|
expect.fail('Status should be confirmed');
|
|
throw new Error();
|
|
}
|
|
expect(jobStatus.transactions[0]).to.contain({ hash: '0x02', timestamp: +transaction2Time.valueOf() });
|
|
});
|
|
|
|
it('should throw if the job is successful but there are no successful transactions', async () => {
|
|
const now = Date.now();
|
|
const transaction1Time = now + 10;
|
|
const transaction2Time = now + 20;
|
|
|
|
const job = new RfqmV2JobEntity({
|
|
...BASE_JOB,
|
|
status: RfqmJobStatus.SucceededUnconfirmed,
|
|
});
|
|
|
|
const submission1 = new RfqmV2TransactionSubmissionEntity({
|
|
createdAt: new Date(transaction1Time),
|
|
orderHash: job.orderHash,
|
|
transactionHash: '0x01',
|
|
status: RfqmTransactionSubmissionStatus.DroppedAndReplaced,
|
|
from: job.order.order.txOrigin,
|
|
to: job.order.order.verifyingContract,
|
|
type: RfqmTransactionSubmissionType.Trade,
|
|
nonce: 0,
|
|
});
|
|
const submission2 = new RfqmV2TransactionSubmissionEntity({
|
|
createdAt: new Date(transaction2Time),
|
|
orderHash: job.orderHash,
|
|
transactionHash: '0x02',
|
|
status: RfqmTransactionSubmissionStatus.RevertedUnconfirmed,
|
|
from: job.order.order.txOrigin,
|
|
to: job.order.order.verifyingContract,
|
|
type: RfqmTransactionSubmissionType.Trade,
|
|
nonce: 1,
|
|
});
|
|
|
|
const dbUtilsMock = mock(RfqmDbUtils);
|
|
when(dbUtilsMock.findV2JobByOrderHashAsync(anything())).thenResolve(job);
|
|
when(
|
|
dbUtilsMock.findV2TransactionSubmissionsByOrderHashAsync(
|
|
job.orderHash,
|
|
deepEqual([
|
|
RfqmTransactionSubmissionType.Trade,
|
|
RfqmTransactionSubmissionType.ApprovalAndTrade,
|
|
]),
|
|
),
|
|
).thenResolve([submission1, submission2]);
|
|
const service = buildRfqmServiceForUnitTest({ dbUtils: instance(dbUtilsMock) });
|
|
|
|
try {
|
|
await service.getStatusAsync('0x00');
|
|
expect.fail();
|
|
} catch (e) {
|
|
expect(e.message).to.contain('Expected exactly one successful transaction submission');
|
|
}
|
|
});
|
|
|
|
it('should throw if the job is successful but there are multiple successful transactions', async () => {
|
|
const now = Date.now();
|
|
const transaction1Time = now + 10;
|
|
const transaction2Time = now + 20;
|
|
|
|
const job = new RfqmV2JobEntity({
|
|
...BASE_JOB,
|
|
status: RfqmJobStatus.SucceededUnconfirmed,
|
|
});
|
|
|
|
const submission1 = new RfqmV2TransactionSubmissionEntity({
|
|
createdAt: new Date(transaction1Time),
|
|
orderHash: job.orderHash,
|
|
transactionHash: '0x01',
|
|
status: RfqmTransactionSubmissionStatus.SucceededUnconfirmed,
|
|
from: job.order.order.txOrigin,
|
|
to: job.order.order.verifyingContract,
|
|
type: RfqmTransactionSubmissionType.Trade,
|
|
nonce: 0,
|
|
});
|
|
const submission2 = new RfqmV2TransactionSubmissionEntity({
|
|
createdAt: new Date(transaction2Time),
|
|
orderHash: job.orderHash,
|
|
transactionHash: '0x02',
|
|
status: RfqmTransactionSubmissionStatus.SucceededUnconfirmed,
|
|
from: job.order.order.txOrigin,
|
|
to: job.order.order.verifyingContract,
|
|
type: RfqmTransactionSubmissionType.Trade,
|
|
nonce: 1,
|
|
});
|
|
|
|
const dbUtilsMock = mock(RfqmDbUtils);
|
|
when(dbUtilsMock.findV2JobByOrderHashAsync(anything())).thenResolve(job);
|
|
when(
|
|
dbUtilsMock.findV2TransactionSubmissionsByOrderHashAsync(
|
|
job.orderHash,
|
|
deepEqual([
|
|
RfqmTransactionSubmissionType.Trade,
|
|
RfqmTransactionSubmissionType.ApprovalAndTrade,
|
|
]),
|
|
),
|
|
).thenResolve([submission1, submission2]);
|
|
const service = buildRfqmServiceForUnitTest({ dbUtils: instance(dbUtilsMock) });
|
|
|
|
try {
|
|
await service.getStatusAsync('0x00');
|
|
expect.fail();
|
|
} catch (e) {
|
|
expect(e.message).to.contain('Expected exactly one successful transaction submission');
|
|
}
|
|
});
|
|
|
|
it('should return submitted with approval and trade transaction submissions for submitted jobs', async () => {
|
|
const now = Date.now();
|
|
const approvalTransaction1Time = now + 3;
|
|
const approvalTransaction2Time = now + 7;
|
|
const tradeTransaction1Time = now + 10;
|
|
const tradeTransaction2Time = now + 20;
|
|
|
|
const job = new RfqmV2JobEntity({
|
|
...BASE_JOB,
|
|
approval: MOCK_EXECUTE_META_TRANSACTION_APPROVAL,
|
|
status: RfqmJobStatus.PendingSubmitted,
|
|
});
|
|
|
|
const approvalSubmission1 = new RfqmV2TransactionSubmissionEntity({
|
|
createdAt: new Date(approvalTransaction1Time),
|
|
orderHash: job.orderHash,
|
|
transactionHash: '0x01',
|
|
from: job.order.order.txOrigin,
|
|
to: job.order.order.verifyingContract,
|
|
type: RfqmTransactionSubmissionType.Approval,
|
|
nonce: 0,
|
|
});
|
|
const approvalSubmission2 = new RfqmV2TransactionSubmissionEntity({
|
|
createdAt: new Date(approvalTransaction2Time),
|
|
orderHash: job.orderHash,
|
|
transactionHash: '0x02',
|
|
from: job.order.order.txOrigin,
|
|
to: job.order.order.verifyingContract,
|
|
type: RfqmTransactionSubmissionType.Approval,
|
|
nonce: 1,
|
|
});
|
|
const tradeSubmission1 = new RfqmV2TransactionSubmissionEntity({
|
|
createdAt: new Date(tradeTransaction1Time),
|
|
orderHash: job.orderHash,
|
|
transactionHash: '0x03',
|
|
from: job.order.order.txOrigin,
|
|
to: job.order.order.verifyingContract,
|
|
type: RfqmTransactionSubmissionType.Trade,
|
|
nonce: 2,
|
|
});
|
|
const tradeSubmission2 = new RfqmV2TransactionSubmissionEntity({
|
|
createdAt: new Date(tradeTransaction2Time),
|
|
orderHash: job.orderHash,
|
|
transactionHash: '0x04',
|
|
from: job.order.order.txOrigin,
|
|
to: job.order.order.verifyingContract,
|
|
type: RfqmTransactionSubmissionType.Trade,
|
|
nonce: 3,
|
|
});
|
|
|
|
const dbUtilsMock = mock(RfqmDbUtils);
|
|
when(dbUtilsMock.findV2JobByOrderHashAsync(anything())).thenResolve(job);
|
|
when(
|
|
dbUtilsMock.findV2TransactionSubmissionsByOrderHashAsync(
|
|
job.orderHash,
|
|
deepEqual([RfqmTransactionSubmissionType.Approval]),
|
|
),
|
|
).thenResolve([approvalSubmission1, approvalSubmission2]);
|
|
when(
|
|
dbUtilsMock.findV2TransactionSubmissionsByOrderHashAsync(
|
|
job.orderHash,
|
|
deepEqual([
|
|
RfqmTransactionSubmissionType.Trade,
|
|
RfqmTransactionSubmissionType.ApprovalAndTrade,
|
|
]),
|
|
),
|
|
).thenResolve([tradeSubmission1, tradeSubmission2]);
|
|
const service = buildRfqmServiceForUnitTest({ dbUtils: instance(dbUtilsMock) });
|
|
|
|
const orderStatus = await service.getStatusAsync('0x00');
|
|
|
|
if (orderStatus === null) {
|
|
expect.fail('Status should exist');
|
|
throw new Error();
|
|
}
|
|
|
|
if (orderStatus.status !== 'submitted') {
|
|
expect.fail('Status should be submitted');
|
|
throw new Error();
|
|
}
|
|
expect(orderStatus.approvalTransactions).to.have.length(2);
|
|
expect(orderStatus.approvalTransactions).to.deep.include({
|
|
hash: '0x01',
|
|
timestamp: +approvalTransaction1Time.valueOf(),
|
|
});
|
|
expect(orderStatus.approvalTransactions).to.deep.include({
|
|
hash: '0x02',
|
|
timestamp: +approvalTransaction2Time.valueOf(),
|
|
});
|
|
expect(orderStatus.transactions).to.have.length(2);
|
|
expect(orderStatus.transactions).to.deep.include({
|
|
hash: '0x03',
|
|
timestamp: +tradeTransaction1Time.valueOf(),
|
|
});
|
|
expect(orderStatus.transactions).to.deep.include({
|
|
hash: '0x04',
|
|
timestamp: +tradeTransaction2Time.valueOf(),
|
|
});
|
|
});
|
|
|
|
it('should return failed with approval and trade transaction submissions for failed jobs', async () => {
|
|
const now = Date.now();
|
|
const approvalTransaction1Time = now + 3;
|
|
const approvalTransaction2Time = now + 7;
|
|
const tradeTransaction1Time = now + 10;
|
|
const tradeTransaction2Time = now + 20;
|
|
|
|
const job = new RfqmV2JobEntity({
|
|
...BASE_JOB,
|
|
approval: MOCK_EXECUTE_META_TRANSACTION_APPROVAL,
|
|
status: RfqmJobStatus.FailedExpired,
|
|
});
|
|
|
|
const approvalSubmission1 = new RfqmV2TransactionSubmissionEntity({
|
|
createdAt: new Date(approvalTransaction1Time),
|
|
orderHash: job.orderHash,
|
|
transactionHash: '0x01',
|
|
from: job.order.order.txOrigin,
|
|
to: job.order.order.verifyingContract,
|
|
type: RfqmTransactionSubmissionType.Approval,
|
|
nonce: 0,
|
|
});
|
|
const approvalSubmission2 = new RfqmV2TransactionSubmissionEntity({
|
|
createdAt: new Date(approvalTransaction2Time),
|
|
orderHash: job.orderHash,
|
|
transactionHash: '0x02',
|
|
from: job.order.order.txOrigin,
|
|
to: job.order.order.verifyingContract,
|
|
type: RfqmTransactionSubmissionType.Approval,
|
|
nonce: 1,
|
|
});
|
|
const tradeSubmission1 = new RfqmV2TransactionSubmissionEntity({
|
|
createdAt: new Date(tradeTransaction1Time),
|
|
orderHash: job.orderHash,
|
|
transactionHash: '0x03',
|
|
from: job.order.order.txOrigin,
|
|
to: job.order.order.verifyingContract,
|
|
type: RfqmTransactionSubmissionType.Trade,
|
|
nonce: 2,
|
|
});
|
|
const tradeSubmission2 = new RfqmV2TransactionSubmissionEntity({
|
|
createdAt: new Date(tradeTransaction2Time),
|
|
orderHash: job.orderHash,
|
|
transactionHash: '0x04',
|
|
from: job.order.order.txOrigin,
|
|
to: job.order.order.verifyingContract,
|
|
type: RfqmTransactionSubmissionType.Trade,
|
|
nonce: 3,
|
|
});
|
|
|
|
const dbUtilsMock = mock(RfqmDbUtils);
|
|
when(dbUtilsMock.findV2JobByOrderHashAsync(anything())).thenResolve(job);
|
|
when(
|
|
dbUtilsMock.findV2TransactionSubmissionsByOrderHashAsync(
|
|
job.orderHash,
|
|
deepEqual([RfqmTransactionSubmissionType.Approval]),
|
|
),
|
|
).thenResolve([approvalSubmission1, approvalSubmission2]);
|
|
when(
|
|
dbUtilsMock.findV2TransactionSubmissionsByOrderHashAsync(
|
|
job.orderHash,
|
|
deepEqual([
|
|
RfqmTransactionSubmissionType.Trade,
|
|
RfqmTransactionSubmissionType.ApprovalAndTrade,
|
|
]),
|
|
),
|
|
).thenResolve([tradeSubmission1, tradeSubmission2]);
|
|
const service = buildRfqmServiceForUnitTest({ dbUtils: instance(dbUtilsMock) });
|
|
|
|
const orderStatus = await service.getStatusAsync('0x00');
|
|
|
|
if (orderStatus === null) {
|
|
expect.fail('Status should exist');
|
|
throw new Error();
|
|
}
|
|
|
|
if (orderStatus.status !== 'failed') {
|
|
expect.fail('Status should be failed');
|
|
throw new Error();
|
|
}
|
|
expect(orderStatus.approvalTransactions).to.have.length(2);
|
|
expect(orderStatus.approvalTransactions).to.deep.include({
|
|
hash: '0x01',
|
|
timestamp: +approvalTransaction1Time.valueOf(),
|
|
});
|
|
expect(orderStatus.approvalTransactions).to.deep.include({
|
|
hash: '0x02',
|
|
timestamp: +approvalTransaction2Time.valueOf(),
|
|
});
|
|
expect(orderStatus.transactions).to.have.length(2);
|
|
expect(orderStatus.transactions).to.deep.include({
|
|
hash: '0x03',
|
|
timestamp: +tradeTransaction1Time.valueOf(),
|
|
});
|
|
expect(orderStatus.transactions).to.deep.include({
|
|
hash: '0x04',
|
|
timestamp: +tradeTransaction2Time.valueOf(),
|
|
});
|
|
});
|
|
|
|
it('should return declined for a job that was declined on the last look', async () => {
|
|
const job = new RfqmV2JobEntity({
|
|
...BASE_JOB,
|
|
status: RfqmJobStatus.FailedLastLookDeclined,
|
|
});
|
|
|
|
const dbUtilsMock = mock(RfqmDbUtils);
|
|
//what is this dummy first attempt?
|
|
when(dbUtilsMock.findV2JobByOrderHashAsync(anything())).thenResolve();
|
|
when(dbUtilsMock.findV2JobByOrderHashAsync(anything())).thenResolve(job);
|
|
when(
|
|
dbUtilsMock.findV2TransactionSubmissionsByOrderHashAsync(
|
|
job.orderHash,
|
|
deepEqual([
|
|
RfqmTransactionSubmissionType.Trade,
|
|
RfqmTransactionSubmissionType.ApprovalAndTrade,
|
|
]),
|
|
),
|
|
).thenResolve([]);
|
|
const service = buildRfqmServiceForUnitTest({ dbUtils: instance(dbUtilsMock) });
|
|
|
|
const jobStatus = await service.getStatusAsync('0x00');
|
|
|
|
if (jobStatus === null) {
|
|
expect.fail('Status should exist');
|
|
throw new Error();
|
|
}
|
|
|
|
expect(jobStatus.status).to.eq('failed');
|
|
if (jobStatus.status == 'failed') {
|
|
expect(jobStatus.reason).to.eq('last_look_declined');
|
|
}
|
|
});
|
|
|
|
it('should return succeeded for a successful job, with the succeeded job and include correct `transactions` and `approvalTransactions`', async () => {
|
|
const now = Date.now();
|
|
const approvalTransaction1Time = now + 3;
|
|
const approvalTransaction2Time = now + 7;
|
|
const tradeTransaction1Time = now + 10;
|
|
const tradeTransaction2Time = now + 20;
|
|
|
|
const job = new RfqmV2JobEntity({
|
|
...BASE_JOB,
|
|
approval: MOCK_EXECUTE_META_TRANSACTION_APPROVAL,
|
|
status: RfqmJobStatus.SucceededUnconfirmed,
|
|
});
|
|
|
|
const approvalSubmission1 = new RfqmV2TransactionSubmissionEntity({
|
|
createdAt: new Date(approvalTransaction1Time),
|
|
orderHash: job.orderHash,
|
|
transactionHash: '0x01',
|
|
status: RfqmTransactionSubmissionStatus.DroppedAndReplaced,
|
|
from: job.order.order.txOrigin,
|
|
to: job.order.order.verifyingContract,
|
|
type: RfqmTransactionSubmissionType.Approval,
|
|
nonce: 0,
|
|
});
|
|
const approvalSubmission2 = new RfqmV2TransactionSubmissionEntity({
|
|
createdAt: new Date(approvalTransaction2Time),
|
|
orderHash: job.orderHash,
|
|
transactionHash: '0x02',
|
|
status: RfqmTransactionSubmissionStatus.SucceededUnconfirmed,
|
|
from: job.order.order.txOrigin,
|
|
to: job.order.order.verifyingContract,
|
|
type: RfqmTransactionSubmissionType.Approval,
|
|
nonce: 1,
|
|
});
|
|
const tradeSubmission1 = new RfqmV2TransactionSubmissionEntity({
|
|
createdAt: new Date(tradeTransaction1Time),
|
|
orderHash: job.orderHash,
|
|
transactionHash: '0x03',
|
|
status: RfqmTransactionSubmissionStatus.DroppedAndReplaced,
|
|
from: job.order.order.txOrigin,
|
|
to: job.order.order.verifyingContract,
|
|
type: RfqmTransactionSubmissionType.Trade,
|
|
nonce: 2,
|
|
});
|
|
const tradeSubmission2 = new RfqmV2TransactionSubmissionEntity({
|
|
createdAt: new Date(tradeTransaction2Time),
|
|
orderHash: job.orderHash,
|
|
transactionHash: '0x04',
|
|
status: RfqmTransactionSubmissionStatus.SucceededUnconfirmed,
|
|
from: job.order.order.txOrigin,
|
|
to: job.order.order.verifyingContract,
|
|
type: RfqmTransactionSubmissionType.Trade,
|
|
nonce: 3,
|
|
});
|
|
|
|
const dbUtilsMock = mock(RfqmDbUtils);
|
|
when(dbUtilsMock.findV2JobByOrderHashAsync(anything())).thenResolve(job);
|
|
when(
|
|
dbUtilsMock.findV2TransactionSubmissionsByOrderHashAsync(
|
|
job.orderHash,
|
|
deepEqual([RfqmTransactionSubmissionType.Approval]),
|
|
),
|
|
).thenResolve([approvalSubmission1, approvalSubmission2]);
|
|
when(
|
|
dbUtilsMock.findV2TransactionSubmissionsByOrderHashAsync(
|
|
job.orderHash,
|
|
deepEqual([
|
|
RfqmTransactionSubmissionType.Trade,
|
|
RfqmTransactionSubmissionType.ApprovalAndTrade,
|
|
]),
|
|
),
|
|
).thenResolve([tradeSubmission1, tradeSubmission2]);
|
|
const service = buildRfqmServiceForUnitTest({ dbUtils: instance(dbUtilsMock) });
|
|
|
|
const orderStatus = await service.getStatusAsync('0x00');
|
|
|
|
if (orderStatus === null) {
|
|
expect.fail('Status should exist');
|
|
throw new Error();
|
|
}
|
|
|
|
if (orderStatus.status !== 'succeeded') {
|
|
expect.fail('Status should be succeeded');
|
|
throw new Error();
|
|
}
|
|
|
|
if (!orderStatus.approvalTransactions) {
|
|
expect.fail('Approval transactions not present');
|
|
throw new Error();
|
|
}
|
|
|
|
expect(orderStatus.approvalTransactions[0]).to.contain({
|
|
hash: '0x02',
|
|
timestamp: +approvalTransaction2Time.valueOf(),
|
|
});
|
|
expect(orderStatus.transactions[0]).to.contain({
|
|
hash: '0x04',
|
|
timestamp: +tradeTransaction2Time.valueOf(),
|
|
});
|
|
});
|
|
|
|
it('[ApproveAndTrade] should return confirmed for a successful confirmed job and include correct `transactions` and no `approvalTransactions`', async () => {
|
|
const now = Date.now();
|
|
const tradeTransaction1Time = now + 10;
|
|
const tradeTransaction2Time = now + 20;
|
|
|
|
const job = new RfqmV2JobEntity({
|
|
...BASE_JOB,
|
|
approval: MOCK_EXECUTE_META_TRANSACTION_APPROVAL,
|
|
status: RfqmJobStatus.SucceededConfirmed,
|
|
});
|
|
|
|
const tradeSubmission1 = new RfqmV2TransactionSubmissionEntity({
|
|
createdAt: new Date(tradeTransaction1Time),
|
|
orderHash: job.orderHash,
|
|
transactionHash: '0x03',
|
|
status: RfqmTransactionSubmissionStatus.DroppedAndReplaced,
|
|
from: job.order.order.txOrigin,
|
|
to: job.order.order.verifyingContract,
|
|
type: RfqmTransactionSubmissionType.ApprovalAndTrade,
|
|
nonce: 2,
|
|
});
|
|
const tradeSubmission2 = new RfqmV2TransactionSubmissionEntity({
|
|
createdAt: new Date(tradeTransaction2Time),
|
|
orderHash: job.orderHash,
|
|
transactionHash: '0x04',
|
|
status: RfqmTransactionSubmissionStatus.SucceededConfirmed,
|
|
from: job.order.order.txOrigin,
|
|
to: job.order.order.verifyingContract,
|
|
type: RfqmTransactionSubmissionType.ApprovalAndTrade,
|
|
nonce: 3,
|
|
});
|
|
|
|
const dbUtilsMock = mock(RfqmDbUtils);
|
|
when(dbUtilsMock.findV2JobByOrderHashAsync(anything())).thenResolve(job);
|
|
when(
|
|
dbUtilsMock.findV2TransactionSubmissionsByOrderHashAsync(
|
|
job.orderHash,
|
|
deepEqual([RfqmTransactionSubmissionType.Approval]),
|
|
),
|
|
).thenResolve([]);
|
|
when(
|
|
dbUtilsMock.findV2TransactionSubmissionsByOrderHashAsync(
|
|
job.orderHash,
|
|
deepEqual([
|
|
RfqmTransactionSubmissionType.Trade,
|
|
RfqmTransactionSubmissionType.ApprovalAndTrade,
|
|
]),
|
|
),
|
|
).thenResolve([tradeSubmission1, tradeSubmission2]);
|
|
const service = buildRfqmServiceForUnitTest({ dbUtils: instance(dbUtilsMock) });
|
|
|
|
const orderStatus = await service.getStatusAsync('0x00');
|
|
|
|
if (orderStatus === null) {
|
|
expect.fail('Status should exist');
|
|
throw new Error();
|
|
}
|
|
|
|
if (orderStatus.status !== 'confirmed') {
|
|
expect.fail('Status should be confirmed');
|
|
throw new Error();
|
|
}
|
|
|
|
expect(orderStatus.approvalTransactions).to.be.undefined;
|
|
expect(orderStatus.transactions[0]).to.contain({
|
|
hash: '0x04',
|
|
timestamp: +tradeTransaction2Time.valueOf(),
|
|
});
|
|
});
|
|
|
|
it('should return confirmed for a successful confirmed job and include correct `transactions` and `approvalTransactions`', async () => {
|
|
const now = Date.now();
|
|
const approvalTransaction1Time = now + 3;
|
|
const approvalTransaction2Time = now + 7;
|
|
const tradeTransaction1Time = now + 10;
|
|
const tradeTransaction2Time = now + 20;
|
|
|
|
const job = new RfqmV2JobEntity({
|
|
...BASE_JOB,
|
|
approval: MOCK_EXECUTE_META_TRANSACTION_APPROVAL,
|
|
status: RfqmJobStatus.SucceededConfirmed,
|
|
});
|
|
|
|
const approvalSubmission1 = new RfqmV2TransactionSubmissionEntity({
|
|
createdAt: new Date(approvalTransaction1Time),
|
|
orderHash: job.orderHash,
|
|
transactionHash: '0x01',
|
|
status: RfqmTransactionSubmissionStatus.DroppedAndReplaced,
|
|
from: job.order.order.txOrigin,
|
|
to: job.order.order.verifyingContract,
|
|
type: RfqmTransactionSubmissionType.Approval,
|
|
nonce: 0,
|
|
});
|
|
const approvalSubmission2 = new RfqmV2TransactionSubmissionEntity({
|
|
createdAt: new Date(approvalTransaction2Time),
|
|
orderHash: job.orderHash,
|
|
transactionHash: '0x02',
|
|
status: RfqmTransactionSubmissionStatus.SucceededConfirmed,
|
|
from: job.order.order.txOrigin,
|
|
to: job.order.order.verifyingContract,
|
|
type: RfqmTransactionSubmissionType.Approval,
|
|
nonce: 1,
|
|
});
|
|
const tradeSubmission1 = new RfqmV2TransactionSubmissionEntity({
|
|
createdAt: new Date(tradeTransaction1Time),
|
|
orderHash: job.orderHash,
|
|
transactionHash: '0x03',
|
|
status: RfqmTransactionSubmissionStatus.DroppedAndReplaced,
|
|
from: job.order.order.txOrigin,
|
|
to: job.order.order.verifyingContract,
|
|
type: RfqmTransactionSubmissionType.Trade,
|
|
nonce: 2,
|
|
});
|
|
const tradeSubmission2 = new RfqmV2TransactionSubmissionEntity({
|
|
createdAt: new Date(tradeTransaction2Time),
|
|
orderHash: job.orderHash,
|
|
transactionHash: '0x04',
|
|
status: RfqmTransactionSubmissionStatus.SucceededConfirmed,
|
|
from: job.order.order.txOrigin,
|
|
to: job.order.order.verifyingContract,
|
|
type: RfqmTransactionSubmissionType.Trade,
|
|
nonce: 3,
|
|
});
|
|
|
|
const dbUtilsMock = mock(RfqmDbUtils);
|
|
when(dbUtilsMock.findV2JobByOrderHashAsync(anything())).thenResolve(job);
|
|
when(
|
|
dbUtilsMock.findV2TransactionSubmissionsByOrderHashAsync(
|
|
job.orderHash,
|
|
deepEqual([RfqmTransactionSubmissionType.Approval]),
|
|
),
|
|
).thenResolve([approvalSubmission1, approvalSubmission2]);
|
|
when(
|
|
dbUtilsMock.findV2TransactionSubmissionsByOrderHashAsync(
|
|
job.orderHash,
|
|
deepEqual([
|
|
RfqmTransactionSubmissionType.Trade,
|
|
RfqmTransactionSubmissionType.ApprovalAndTrade,
|
|
]),
|
|
),
|
|
).thenResolve([tradeSubmission1, tradeSubmission2]);
|
|
const service = buildRfqmServiceForUnitTest({ dbUtils: instance(dbUtilsMock) });
|
|
|
|
const orderStatus = await service.getStatusAsync('0x00');
|
|
|
|
if (orderStatus === null) {
|
|
expect.fail('Status should exist');
|
|
throw new Error();
|
|
}
|
|
|
|
if (orderStatus.status !== 'confirmed') {
|
|
expect.fail('Status should be confirmed');
|
|
throw new Error();
|
|
}
|
|
|
|
if (!orderStatus.approvalTransactions) {
|
|
expect.fail('Approval transactions not present');
|
|
throw new Error();
|
|
}
|
|
|
|
expect(orderStatus.approvalTransactions[0]).to.contain({
|
|
hash: '0x02',
|
|
timestamp: +approvalTransaction2Time.valueOf(),
|
|
});
|
|
expect(orderStatus.transactions[0]).to.contain({
|
|
hash: '0x04',
|
|
timestamp: +tradeTransaction2Time.valueOf(),
|
|
});
|
|
});
|
|
|
|
it('should throw if the job is successful but there are no successful transactions for approval', async () => {
|
|
const now = Date.now();
|
|
const approvalTransaction1Time = now + 3;
|
|
const approvalTransaction2Time = now + 7;
|
|
const tradeTransaction1Time = now + 10;
|
|
const tradeTransaction2Time = now + 20;
|
|
|
|
const job = new RfqmV2JobEntity({
|
|
...BASE_JOB,
|
|
approval: MOCK_EXECUTE_META_TRANSACTION_APPROVAL,
|
|
status: RfqmJobStatus.SucceededUnconfirmed,
|
|
});
|
|
|
|
const approvalSubmission1 = new RfqmV2TransactionSubmissionEntity({
|
|
createdAt: new Date(approvalTransaction1Time),
|
|
orderHash: job.orderHash,
|
|
transactionHash: '0x01',
|
|
status: RfqmTransactionSubmissionStatus.DroppedAndReplaced,
|
|
from: job.order.order.txOrigin,
|
|
to: job.order.order.verifyingContract,
|
|
type: RfqmTransactionSubmissionType.Approval,
|
|
nonce: 0,
|
|
});
|
|
const approvalSubmission2 = new RfqmV2TransactionSubmissionEntity({
|
|
createdAt: new Date(approvalTransaction2Time),
|
|
orderHash: job.orderHash,
|
|
transactionHash: '0x02',
|
|
status: RfqmTransactionSubmissionStatus.RevertedUnconfirmed,
|
|
from: job.order.order.txOrigin,
|
|
to: job.order.order.verifyingContract,
|
|
type: RfqmTransactionSubmissionType.Approval,
|
|
nonce: 1,
|
|
});
|
|
const tradeSubmission1 = new RfqmV2TransactionSubmissionEntity({
|
|
createdAt: new Date(tradeTransaction1Time),
|
|
orderHash: job.orderHash,
|
|
transactionHash: '0x03',
|
|
status: RfqmTransactionSubmissionStatus.DroppedAndReplaced,
|
|
from: job.order.order.txOrigin,
|
|
to: job.order.order.verifyingContract,
|
|
type: RfqmTransactionSubmissionType.Trade,
|
|
nonce: 2,
|
|
});
|
|
const tradeSubmission2 = new RfqmV2TransactionSubmissionEntity({
|
|
createdAt: new Date(tradeTransaction2Time),
|
|
orderHash: job.orderHash,
|
|
transactionHash: '0x04',
|
|
status: RfqmTransactionSubmissionStatus.SucceededUnconfirmed,
|
|
from: job.order.order.txOrigin,
|
|
to: job.order.order.verifyingContract,
|
|
type: RfqmTransactionSubmissionType.Trade,
|
|
nonce: 3,
|
|
});
|
|
|
|
const dbUtilsMock = mock(RfqmDbUtils);
|
|
when(dbUtilsMock.findV2JobByOrderHashAsync(anything())).thenResolve(job);
|
|
when(
|
|
dbUtilsMock.findV2TransactionSubmissionsByOrderHashAsync(
|
|
job.orderHash,
|
|
deepEqual([RfqmTransactionSubmissionType.Approval]),
|
|
),
|
|
).thenResolve([approvalSubmission1, approvalSubmission2]);
|
|
when(
|
|
dbUtilsMock.findV2TransactionSubmissionsByOrderHashAsync(
|
|
job.orderHash,
|
|
deepEqual([
|
|
RfqmTransactionSubmissionType.Trade,
|
|
RfqmTransactionSubmissionType.ApprovalAndTrade,
|
|
]),
|
|
),
|
|
).thenResolve([tradeSubmission1, tradeSubmission2]);
|
|
const service = buildRfqmServiceForUnitTest({ dbUtils: instance(dbUtilsMock) });
|
|
|
|
try {
|
|
await service.getStatusAsync('0x00');
|
|
expect.fail();
|
|
} catch (e) {
|
|
expect(e.message).to.contain('Expected exactly one successful transaction submission');
|
|
}
|
|
});
|
|
|
|
it('should throw if the job is successful but there are multiple successful transactions for approval', async () => {
|
|
const now = Date.now();
|
|
const approvalTransaction1Time = now + 3;
|
|
const approvalTransaction2Time = now + 7;
|
|
const tradeTransaction1Time = now + 10;
|
|
const tradeTransaction2Time = now + 20;
|
|
|
|
const job = new RfqmV2JobEntity({
|
|
...BASE_JOB,
|
|
approval: MOCK_EXECUTE_META_TRANSACTION_APPROVAL,
|
|
status: RfqmJobStatus.SucceededUnconfirmed,
|
|
});
|
|
|
|
const approvalSubmission1 = new RfqmV2TransactionSubmissionEntity({
|
|
createdAt: new Date(approvalTransaction1Time),
|
|
orderHash: job.orderHash,
|
|
transactionHash: '0x01',
|
|
status: RfqmTransactionSubmissionStatus.SucceededUnconfirmed,
|
|
from: job.order.order.txOrigin,
|
|
to: job.order.order.verifyingContract,
|
|
type: RfqmTransactionSubmissionType.Approval,
|
|
nonce: 0,
|
|
});
|
|
const approvalSubmission2 = new RfqmV2TransactionSubmissionEntity({
|
|
createdAt: new Date(approvalTransaction2Time),
|
|
orderHash: job.orderHash,
|
|
transactionHash: '0x02',
|
|
status: RfqmTransactionSubmissionStatus.SucceededConfirmed,
|
|
from: job.order.order.txOrigin,
|
|
to: job.order.order.verifyingContract,
|
|
type: RfqmTransactionSubmissionType.Approval,
|
|
nonce: 1,
|
|
});
|
|
const tradeSubmission1 = new RfqmV2TransactionSubmissionEntity({
|
|
createdAt: new Date(tradeTransaction1Time),
|
|
orderHash: job.orderHash,
|
|
transactionHash: '0x03',
|
|
status: RfqmTransactionSubmissionStatus.DroppedAndReplaced,
|
|
from: job.order.order.txOrigin,
|
|
to: job.order.order.verifyingContract,
|
|
type: RfqmTransactionSubmissionType.Trade,
|
|
nonce: 2,
|
|
});
|
|
const tradeSubmission2 = new RfqmV2TransactionSubmissionEntity({
|
|
createdAt: new Date(tradeTransaction2Time),
|
|
orderHash: job.orderHash,
|
|
transactionHash: '0x04',
|
|
status: RfqmTransactionSubmissionStatus.SucceededUnconfirmed,
|
|
from: job.order.order.txOrigin,
|
|
to: job.order.order.verifyingContract,
|
|
type: RfqmTransactionSubmissionType.Trade,
|
|
nonce: 3,
|
|
});
|
|
|
|
const dbUtilsMock = mock(RfqmDbUtils);
|
|
when(dbUtilsMock.findV2JobByOrderHashAsync(anything())).thenResolve(job);
|
|
when(
|
|
dbUtilsMock.findV2TransactionSubmissionsByOrderHashAsync(
|
|
job.orderHash,
|
|
deepEqual([RfqmTransactionSubmissionType.Approval]),
|
|
),
|
|
).thenResolve([approvalSubmission1, approvalSubmission2]);
|
|
when(
|
|
dbUtilsMock.findV2TransactionSubmissionsByOrderHashAsync(
|
|
job.orderHash,
|
|
deepEqual([
|
|
RfqmTransactionSubmissionType.Trade,
|
|
RfqmTransactionSubmissionType.ApprovalAndTrade,
|
|
]),
|
|
),
|
|
).thenResolve([tradeSubmission1, tradeSubmission2]);
|
|
const service = buildRfqmServiceForUnitTest({ dbUtils: instance(dbUtilsMock) });
|
|
|
|
try {
|
|
await service.getStatusAsync('0x00');
|
|
expect.fail();
|
|
} catch (e) {
|
|
expect(e.message).to.contain('Expected exactly one successful transaction submission');
|
|
}
|
|
});
|
|
|
|
it('should throw if the job is successful but the successful transaciton has no hash for approval', async () => {
|
|
const now = Date.now();
|
|
const approvalTransaction1Time = now + 3;
|
|
const approvalTransaction2Time = now + 7;
|
|
const tradeTransaction1Time = now + 10;
|
|
const tradeTransaction2Time = now + 20;
|
|
|
|
const job = new RfqmV2JobEntity({
|
|
...BASE_JOB,
|
|
approval: MOCK_EXECUTE_META_TRANSACTION_APPROVAL,
|
|
status: RfqmJobStatus.SucceededUnconfirmed,
|
|
});
|
|
|
|
const approvalSubmission1 = new RfqmV2TransactionSubmissionEntity({
|
|
createdAt: new Date(approvalTransaction1Time),
|
|
orderHash: job.orderHash,
|
|
transactionHash: '0x01',
|
|
status: RfqmTransactionSubmissionStatus.DroppedAndReplaced,
|
|
from: job.order.order.txOrigin,
|
|
to: job.order.order.verifyingContract,
|
|
type: RfqmTransactionSubmissionType.Approval,
|
|
nonce: 0,
|
|
});
|
|
const approvalSubmission2 = new RfqmV2TransactionSubmissionEntity({
|
|
createdAt: new Date(approvalTransaction2Time),
|
|
orderHash: job.orderHash,
|
|
transactionHash: '',
|
|
status: RfqmTransactionSubmissionStatus.SucceededConfirmed,
|
|
from: job.order.order.txOrigin,
|
|
to: job.order.order.verifyingContract,
|
|
type: RfqmTransactionSubmissionType.Approval,
|
|
nonce: 1,
|
|
});
|
|
const tradeSubmission1 = new RfqmV2TransactionSubmissionEntity({
|
|
createdAt: new Date(tradeTransaction1Time),
|
|
orderHash: job.orderHash,
|
|
transactionHash: '0x03',
|
|
status: RfqmTransactionSubmissionStatus.DroppedAndReplaced,
|
|
from: job.order.order.txOrigin,
|
|
to: job.order.order.verifyingContract,
|
|
type: RfqmTransactionSubmissionType.Trade,
|
|
nonce: 2,
|
|
});
|
|
const tradeSubmission2 = new RfqmV2TransactionSubmissionEntity({
|
|
createdAt: new Date(tradeTransaction2Time),
|
|
orderHash: job.orderHash,
|
|
transactionHash: '0x04',
|
|
status: RfqmTransactionSubmissionStatus.SucceededUnconfirmed,
|
|
from: job.order.order.txOrigin,
|
|
to: job.order.order.verifyingContract,
|
|
type: RfqmTransactionSubmissionType.Trade,
|
|
nonce: 3,
|
|
});
|
|
|
|
const dbUtilsMock = mock(RfqmDbUtils);
|
|
when(dbUtilsMock.findV2JobByOrderHashAsync(anything())).thenResolve(job);
|
|
when(
|
|
dbUtilsMock.findV2TransactionSubmissionsByOrderHashAsync(
|
|
job.orderHash,
|
|
deepEqual([RfqmTransactionSubmissionType.Approval]),
|
|
),
|
|
).thenResolve([approvalSubmission1, approvalSubmission2]);
|
|
when(
|
|
dbUtilsMock.findV2TransactionSubmissionsByOrderHashAsync(
|
|
job.orderHash,
|
|
deepEqual([
|
|
RfqmTransactionSubmissionType.Trade,
|
|
RfqmTransactionSubmissionType.ApprovalAndTrade,
|
|
]),
|
|
),
|
|
).thenResolve([tradeSubmission1, tradeSubmission2]);
|
|
const service = buildRfqmServiceForUnitTest({ dbUtils: instance(dbUtilsMock) });
|
|
|
|
try {
|
|
await service.getStatusAsync('0x00');
|
|
expect.fail();
|
|
} catch (e) {
|
|
expect(e.message).to.contain('does not have a hash');
|
|
}
|
|
});
|
|
});
|
|
});
|
|
});
|