Files
protocol/apps-node/rfq-api/test/services/rfqm_service_test.ts
2023-04-07 11:13:18 -07:00

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');
}
});
});
});
});