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

8420 lines
383 KiB
TypeScript

import { pino } from '@0x/api-utils';
import { OtcOrder, SignatureType } from '@0x/protocol-utils';
import { BigNumber } from '@0x/utils';
import { expect } from 'chai';
import { BigNumber as EthersBigNumber, providers } from 'ethersv5';
import * as _ from 'lodash';
import { Producer } from 'sqs-producer';
import { anything, capture, deepEqual, instance, mock, spy, verify, when } from 'ts-mockito';
import { ETH_DECIMALS, GWEI_DECIMALS, ONE_SECOND_MS } from '../../src/core/constants';
import {
MetaTransactionJobEntity,
RfqmJobEntity,
RfqmTransactionSubmissionEntity,
RfqmV2JobEntity,
RfqmV2QuoteEntity,
RfqmV2TransactionSubmissionEntity,
} from '../../src/entities';
import { MetaTransactionJobConstructorOpts } from '../../src/entities/MetaTransactionJobEntity';
import {
MetaTransactionSubmissionEntity,
MetaTransactionSubmissionEntityConstructorOpts,
} from '../../src/entities/MetaTransactionSubmissionEntity';
import {
MetaTransactionV2JobConstructorOpts,
MetaTransactionV2JobEntity,
} from '../../src/entities/MetaTransactionV2JobEntity';
import {
MetaTransactionV2SubmissionEntity,
MetaTransactionV2SubmissionEntityConstructorOpts,
} from '../../src/entities/MetaTransactionV2SubmissionEntity';
import {
RfqmJobStatus,
RfqmOrderTypes,
RfqmTransactionSubmissionStatus,
RfqmTransactionSubmissionType,
SubmissionContextStatus,
} from '../../src/entities/types';
import { logger } from '../../src/logger';
import { RfqMakerBalanceCacheService } from '../../src/services/rfq_maker_balance_cache_service';
import { WorkerService } from '../../src/services/WorkerService';
import { CacheClient } from '../../src/utils/cache_client';
import { GasStationAttendant } from '../../src/utils/GasStationAttendant';
import { GasStationAttendantEthereum } from '../../src/utils/GasStationAttendantEthereum';
import { QuoteServerClient } from '../../src/utils/quote_server_client';
import { RfqmDbUtils } from '../../src/utils/rfqm_db_utils';
import { RfqBlockchainUtils } from '../../src/utils/rfq_blockchain_utils';
import { RfqMakerManager } from '../../src/utils/rfq_maker_manager';
import { padSignature } from '../../src/utils/signature_utils';
import { SubmissionContext } from '../../src/utils/SubmissionContext';
import {
MOCK_EXECUTE_META_TRANSACTION_APPROVAL,
MOCK_EXECUTE_META_TRANSACTION_CALLDATA,
MOCK_META_TRANSACTION,
MOCK_META_TRANSACTION_V2,
MOCK_PERMIT_APPROVAL,
} from '../constants';
// $eslint-fix-me https://github.com/rhinodavid/eslint-fix-me
// eslint-disable-next-line @typescript-eslint/no-loss-of-precision
const MOCK_WORKER_REGISTRY_ADDRESS = '0x1023331a469c6391730ff1E2749422CE8873EC38';
const MOCK_GAS_PRICE = new BigNumber(100);
const TEST_RFQM_TRANSACTION_WATCHER_SLEEP_TIME_MS = 50;
const WORKER_FULL_BALANCE_WEI = new BigNumber(1).shiftedBy(ETH_DECIMALS);
let loggerSpy: pino.Logger;
const buildWorkerServiceForUnitTest = (
overrides: {
cacheClient?: CacheClient;
dbUtils?: RfqmDbUtils;
rfqMakerBalanceCacheService?: RfqMakerBalanceCacheService;
rfqMakerManager?: RfqMakerManager;
gasStationAttendant?: GasStationAttendant;
quoteServerClient?: QuoteServerClient;
rfqBlockchainUtils?: RfqBlockchainUtils;
initialMaxPriorityFeePerGasGwei?: number;
maxFeePerGasGwei?: number;
enableAccessList?: boolean;
} = {},
): WorkerService => {
const gasStationAttendantMock = mock(GasStationAttendantEthereum);
when(gasStationAttendantMock.getExpectedTransactionGasRateAsync()).thenResolve(MOCK_GAS_PRICE);
const gasStationAttendantInstance = instance(gasStationAttendantMock);
const rfqBlockchainUtilsMock = mock(RfqBlockchainUtils);
when(rfqBlockchainUtilsMock.getAccountBalanceAsync(MOCK_WORKER_REGISTRY_ADDRESS)).thenResolve(
WORKER_FULL_BALANCE_WEI,
);
const sqsMock = mock(Producer);
when(sqsMock.queueSize()).thenResolve(0);
const quoteServerClientMock = mock(QuoteServerClient);
const cacheClientMock = mock(CacheClient);
const defaultDbUtilsMock = mock(RfqmDbUtils);
const rfqMakerBalanceCacheServiceMock = mock(RfqMakerBalanceCacheService);
const rfqMakerManagerMock = mock(RfqMakerManager);
return new WorkerService(
1,
overrides.gasStationAttendant || gasStationAttendantInstance,
MOCK_WORKER_REGISTRY_ADDRESS,
overrides.rfqBlockchainUtils || instance(rfqBlockchainUtilsMock),
overrides.dbUtils || instance(defaultDbUtilsMock),
overrides.quoteServerClient || quoteServerClientMock,
TEST_RFQM_TRANSACTION_WATCHER_SLEEP_TIME_MS,
overrides.cacheClient || cacheClientMock,
overrides.rfqMakerBalanceCacheService || instance(rfqMakerBalanceCacheServiceMock),
overrides.rfqMakerManager || rfqMakerManagerMock,
overrides.initialMaxPriorityFeePerGasGwei || 2,
overrides.maxFeePerGasGwei || 128,
overrides.enableAccessList,
);
};
const createMetaTransactionJobEntity = (
opts: MetaTransactionJobConstructorOpts,
id: string,
): MetaTransactionJobEntity => {
const job = new MetaTransactionJobEntity(opts);
job.id = id;
return job;
};
const createMetaTransactionV2JobEntity = (
opts: MetaTransactionV2JobConstructorOpts,
id: string,
): MetaTransactionV2JobEntity => {
const job = new MetaTransactionV2JobEntity(opts);
job.id = id;
return job;
};
const createMetaTransactionSubmissionEntity = (
opts: MetaTransactionSubmissionEntityConstructorOpts,
id: string,
): MetaTransactionSubmissionEntity => {
const submission = new MetaTransactionSubmissionEntity(opts);
submission.id = id;
return submission;
};
const createMetaTransactionV2SubmissionEntity = (
opts: MetaTransactionV2SubmissionEntityConstructorOpts,
id: string,
): MetaTransactionV2SubmissionEntity => {
const submission = new MetaTransactionV2SubmissionEntity(opts);
submission.id = id;
return submission;
};
const fakeClockMs = 1637722898000;
const fakeOneMinuteAgoS = fakeClockMs / ONE_SECOND_MS - 60;
const fakeFiveMinutesLater = fakeClockMs / ONE_SECOND_MS + 300;
const maker = '0xbb004090d26845b672f17c6da4b7d162df3bfc5e';
const orderHash = '0x112160fb0933ecde720f63b50b303ce64e52ded702bef78b9c20361f3652a462';
// This sig actually belongs to the maker above
const validEIP712Sig = {
signatureType: SignatureType.EIP712,
v: 28,
r: '0xdc158f7b53b940863bc7b001552a90282e51033f29b73d44a2701bd16faa19d2',
s: '0x55f6c5470e41b39a5ddeb63c22f8ba1d34748f93265715b9dc4a0f10138985a6',
};
// This is a real signature that had a missing byte
const missingByteSig = {
r: '0x568b31076e1c65954adb1bccc723718b3460f1b699ce1252f8a83bda0d521005',
s: '0x0307cc7f4161df812f7e5a651b23dbd33981c0410df0dd820a52f61be7a5ab',
v: 28,
signatureType: SignatureType.EthSign,
};
jest.setTimeout(ONE_SECOND_MS * 120);
describe('WorkerService', () => {
beforeEach(() => {
loggerSpy = spy(logger);
});
describe('workerBeforeLogicAsync', () => {
it('calls `processJobAsync` with the correct arguments', async () => {
const workerIndex = 0;
const workerAddress = MOCK_WORKER_REGISTRY_ADDRESS;
const jobId = 'jobId';
const metaTransactionJob = createMetaTransactionJobEntity(
{
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
fee: {
amount: new BigNumber(0),
token: '',
type: 'fixed',
},
inputToken: '0xinputToken',
inputTokenAmount: new BigNumber(10),
integratorId: '0xintegrator',
metaTransaction: MOCK_META_TRANSACTION,
metaTransactionHash: MOCK_META_TRANSACTION.getHash(),
minOutputTokenAmount: new BigNumber(10),
outputToken: '0xoutputToken',
takerAddress: '0xtakerAddress',
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
status: RfqmJobStatus.PendingEnqueued,
workerAddress: '0xworkeraddress',
},
jobId,
);
const metaTransactionV2Job = createMetaTransactionV2JobEntity(
{
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
inputToken: '0xinputToken',
inputTokenAmount: new BigNumber(10),
integratorId: '0xintegrator',
calledFunction: 'transformERC20',
metaTransaction: MOCK_META_TRANSACTION_V2,
metaTransactionHash: MOCK_META_TRANSACTION_V2.getHash(),
minOutputTokenAmount: new BigNumber(10),
outputToken: '0xoutputToken',
takerAddress: '0xtakerAddress',
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
tokens: ['0xinputToken', '0xoutputToken'],
status: RfqmJobStatus.PendingEnqueued,
workerAddress: '0xworkeraddress',
},
jobId,
);
const rfqmV2Job = new RfqmV2JobEntity({
affiliateAddress: '',
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
fee: {
amount: '0',
token: '',
type: 'fixed',
},
integratorId: '',
lastLookResult: null,
makerUri: 'http://foo.bar',
makerSignature: null,
order: {
order: {
chainId: '1',
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
new BigNumber(fakeFiveMinutesLater),
new BigNumber(1),
new BigNumber(1),
).toString(),
maker,
makerAmount: '1000000',
makerToken: '0xmakertoken',
taker: '0xtaker',
takerAmount: '10000000',
takerToken: '0xtakertoken',
txOrigin: '',
verifyingContract: '',
},
type: RfqmOrderTypes.Otc,
},
orderHash,
status: RfqmJobStatus.PendingEnqueued,
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
updatedAt: new Date(),
workerAddress,
workflow: 'rfqm',
});
const gasStationAttendantMock = mock(GasStationAttendantEthereum);
when(gasStationAttendantMock.getExpectedTransactionGasRateAsync()).thenResolve(
new BigNumber(10).shiftedBy(GWEI_DECIMALS),
);
const blockchainUtilsMock = mock(RfqBlockchainUtils);
when(blockchainUtilsMock.getAccountBalanceAsync(workerAddress)).thenResolve(WORKER_FULL_BALANCE_WEI);
const dbUtilsMock = mock(RfqmDbUtils);
when(dbUtilsMock.findV2UnresolvedJobsAsync(workerAddress, anything())).thenResolve([rfqmV2Job]);
when(dbUtilsMock.findUnresolvedMetaTransactionJobsAsync(workerAddress, anything())).thenResolve([
metaTransactionJob,
]);
when(dbUtilsMock.findUnresolvedMetaTransactionV2JobsAsync(workerAddress, anything())).thenResolve([
metaTransactionV2Job,
]);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(dbUtilsMock),
rfqBlockchainUtils: instance(blockchainUtilsMock),
gasStationAttendant: instance(gasStationAttendantMock),
});
const spiedRfqmService = spy(rfqmService);
when(spiedRfqmService.processJobAsync(anything(), anything(), anything())).thenResolve();
await rfqmService.workerBeforeLogicAsync(workerIndex, workerAddress);
verify(spiedRfqmService.processJobAsync(orderHash, workerAddress, 'rfqm_v2_job')).once();
verify(spiedRfqmService.processJobAsync(jobId, workerAddress, 'meta_transaction_job')).once();
verify(spiedRfqmService.processJobAsync(jobId, workerAddress, 'meta_transaction_v2_job')).once();
});
});
describe('processJobAsync', () => {
it('fails if no rfqm v2 job is found', async () => {
// Return `undefined` for v1 and v2 job for orderhash
const dbUtilsMock = mock(RfqmDbUtils);
when(dbUtilsMock.findV2JobByOrderHashAsync('0x01234567')).thenResolve(null);
const rfqmService = buildWorkerServiceForUnitTest({ dbUtils: instance(dbUtilsMock) });
await rfqmService.processJobAsync('0x01234567', '0xworkeraddress');
expect(capture(loggerSpy.error).last()[0]).to.include({
errorMessage: 'No job found for identifier',
});
});
it('fails if a worker ends up with a job assigned to a different worker for a rfqm v2 job', async () => {
const dbUtilsMock = mock(RfqmDbUtils);
when(dbUtilsMock.findV2JobByOrderHashAsync('0x01234567')).thenResolve(
new RfqmV2JobEntity({
affiliateAddress: '',
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeOneMinuteAgoS),
fee: {
amount: '0',
token: '',
type: 'fixed',
},
integratorId: '',
lastLookResult: null,
makerUri: 'http://foo.bar',
order: {
order: {
chainId: '1',
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
new BigNumber(fakeOneMinuteAgoS.toString()),
new BigNumber(1),
new BigNumber(1),
).toString(),
maker: '',
makerAmount: '',
makerToken: '',
taker: '',
takerAmount: '',
takerToken: '',
txOrigin: '',
verifyingContract: '',
},
type: RfqmOrderTypes.Otc,
},
orderHash: '0x01234567',
status: RfqmJobStatus.PendingEnqueued,
updatedAt: new Date(),
workerAddress: '0xwrongworkeraddress',
workflow: 'rfqm',
}),
);
const rfqmService = buildWorkerServiceForUnitTest({ dbUtils: instance(dbUtilsMock) });
await rfqmService.processJobAsync('0x01234567', '0xworkeraddress');
expect(capture(loggerSpy.error).last()[0]).to.include({
errorMessage: 'Worker was sent a job claimed by a different worker',
});
});
it('fails if no meta-transaction job is found', async () => {
const dbUtilsMock = mock(RfqmDbUtils);
const jobId = 'jobId';
when(dbUtilsMock.findMetaTransactionJobByIdAsync(jobId)).thenResolve(null);
const rfqmService = buildWorkerServiceForUnitTest({ dbUtils: instance(dbUtilsMock) });
await rfqmService.processJobAsync(jobId, '0xworkeraddress', 'meta_transaction_job');
expect(capture(loggerSpy.error).last()[0]).to.include({
errorMessage: 'No job found for identifier',
});
});
it('fails if a worker ends up with a job assigned to a different worker for a meta-transaction job', async () => {
const jobId = 'jobId';
const job = createMetaTransactionJobEntity(
{
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
fee: {
amount: new BigNumber(0),
token: '',
type: 'fixed',
},
inputToken: 'inputToken',
inputTokenAmount: new BigNumber(10),
integratorId: '0xintegrator',
metaTransaction: MOCK_META_TRANSACTION,
metaTransactionHash: MOCK_META_TRANSACTION.getHash(),
minOutputTokenAmount: new BigNumber(10),
outputToken: '0xoutputToken',
takerAddress: '0xtakerAddress',
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
status: RfqmJobStatus.PendingEnqueued,
workerAddress: '0xwrongworkeraddress',
approval: MOCK_EXECUTE_META_TRANSACTION_APPROVAL,
approvalSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
},
jobId,
);
const dbUtilsMock = mock(RfqmDbUtils);
when(dbUtilsMock.findMetaTransactionJobByIdAsync(jobId)).thenResolve(job);
const rfqmService = buildWorkerServiceForUnitTest({ dbUtils: instance(dbUtilsMock) });
await rfqmService.processJobAsync(jobId, '0xworkeraddress', 'meta_transaction_job');
expect(capture(loggerSpy.error).last()[0]).to.include({
errorMessage: 'Worker was sent a job claimed by a different worker',
});
});
it('fails if no meta-transaction v2 job is found', async () => {
const dbUtilsMock = mock(RfqmDbUtils);
const jobId = 'jobId';
when(dbUtilsMock.findMetaTransactionV2JobByIdAsync(jobId)).thenResolve(null);
const rfqmService = buildWorkerServiceForUnitTest({ dbUtils: instance(dbUtilsMock) });
await rfqmService.processJobAsync(jobId, '0xworkeraddress', 'meta_transaction_v2_job');
expect(capture(loggerSpy.error).last()[0]).to.include({
errorMessage: 'No job found for identifier',
});
});
it('fails if a worker ends up with a job assigned to a different worker for a meta-transaction v2 job', async () => {
const jobId = 'jobId';
const job = createMetaTransactionV2JobEntity(
{
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
inputToken: '0xinputToken',
inputTokenAmount: new BigNumber(10),
integratorId: '0xintegrator',
calledFunction: 'transformERC20',
metaTransaction: MOCK_META_TRANSACTION_V2,
metaTransactionHash: MOCK_META_TRANSACTION_V2.getHash(),
minOutputTokenAmount: new BigNumber(10),
outputToken: '0xoutputToken',
takerAddress: '0xtakerAddress',
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
tokens: ['0xinputToken', '0xoutputToken'],
status: RfqmJobStatus.PendingEnqueued,
workerAddress: '0xwrongworkeraddress',
},
jobId,
);
const dbUtilsMock = mock(RfqmDbUtils);
when(dbUtilsMock.findMetaTransactionV2JobByIdAsync(jobId)).thenResolve(job);
const rfqmService = buildWorkerServiceForUnitTest({ dbUtils: instance(dbUtilsMock) });
await rfqmService.processJobAsync(jobId, '0xworkeraddress', 'meta_transaction_v2_job');
expect(capture(loggerSpy.error).last()[0]).to.include({
errorMessage: 'Worker was sent a job claimed by a different worker',
});
});
});
describe('processAtomicApprovalAndTradeAsync', () => {
it('throws if non-approval job is supplied to the method for a rfqm v2 job', async () => {
const job = new RfqmV2JobEntity({
affiliateAddress: '',
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
fee: {
amount: '0',
token: '',
type: 'fixed',
},
integratorId: '',
lastLookResult: null,
makerUri: 'http://foo.bar',
makerSignature: null,
order: {
order: {
chainId: '1',
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
new BigNumber(fakeFiveMinutesLater.toString()),
new BigNumber(1),
new BigNumber(1),
).toString(),
maker: '0xmaker',
makerAmount: '1000000',
makerToken: '0xmakertoken',
taker: '0xtaker',
takerAmount: '10000000',
takerToken: '0xtakertoken',
txOrigin: '',
verifyingContract: '',
},
type: RfqmOrderTypes.Otc,
},
orderHash: '0x01234567',
status: RfqmJobStatus.PendingEnqueued,
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
updatedAt: new Date(),
workerAddress: '',
workflow: 'rfqm',
});
const rfqmService = buildWorkerServiceForUnitTest();
try {
await rfqmService.processAtomicApprovalAndTradeAsync(job, '0xworkeraddress');
expect.fail();
} catch (e) {
expect(e.message).to.contain(
'Non-approval job should not be processed by `processAtomicApprovalAndTradeAsync`',
);
}
});
it('should process an atomic approval and rfqm v2 job trade successfully', async () => {
const nowS = Math.round(new Date().getTime() / ONE_SECOND_MS);
const job = new RfqmV2JobEntity({
affiliateAddress: '',
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(nowS + 60),
fee: {
amount: '0',
token: '',
type: 'fixed',
},
integratorId: '',
lastLookResult: null,
makerUri: 'http://foo.bar',
makerSignature: null,
order: {
order: {
chainId: '1',
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
new BigNumber(nowS + 60),
new BigNumber(1),
new BigNumber(1),
).toString(),
maker,
makerAmount: '1000000',
makerToken: '0xmakertoken',
taker: '0xtaker',
takerAmount: '10000000',
takerToken: '0xtakertoken',
txOrigin: '',
verifyingContract: '',
},
type: RfqmOrderTypes.Otc,
},
orderHash,
status: RfqmJobStatus.PendingEnqueued,
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
approval: MOCK_PERMIT_APPROVAL,
approvalSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
updatedAt: new Date(),
workerAddress: '0xworkeraddress',
workflow: 'rfqm',
});
const mockTransactionRequest: providers.TransactionRequest = {};
const mockTransaction = new RfqmV2TransactionSubmissionEntity({
from: '0xworkeraddress',
maxFeePerGas: new BigNumber(100000),
maxPriorityFeePerGas: new BigNumber(100),
nonce: 0,
orderHash: '0x01234567',
to: '0xexchangeproxyaddress',
transactionHash: '0xsignedtransactionhash',
type: RfqmTransactionSubmissionType.Trade,
});
const mockTransactionReceipt: providers.TransactionReceipt = {
blockHash: '0xblockhash',
blockNumber: 1,
byzantium: true,
confirmations: 3,
contractAddress: '0xexchangeproxyaddress',
cumulativeGasUsed: EthersBigNumber.from(1000),
effectiveGasPrice: EthersBigNumber.from(1000),
from: '0xworkeraddress',
gasUsed: EthersBigNumber.from(10000),
logs: [],
logsBloom: '',
status: 1,
to: '0xexchangeproxyaddress',
transactionHash: '0xsignedtransactionhash',
transactionIndex: 0,
type: 2,
};
const mockMinedBlock: providers.Block = {
_difficulty: EthersBigNumber.from(2),
difficulty: 2,
extraData: '',
gasLimit: EthersBigNumber.from(1000),
gasUsed: EthersBigNumber.from(1000),
hash: '0xblockhash',
miner: '0xminer',
nonce: '0x000',
number: 21,
parentHash: '0xparentblockhash',
timestamp: 12345,
transactions: ['0xpresubmittransactionhash'],
};
const mockNonce = 0;
const gasStationAttendantMock = mock(GasStationAttendantEthereum);
when(gasStationAttendantMock.getExpectedTransactionGasRateAsync()).thenResolve(
new BigNumber(10).shiftedBy(GWEI_DECIMALS),
);
const mockDbUtils = mock(RfqmDbUtils);
when(mockDbUtils.findV2TransactionSubmissionsByOrderHashAsync(anything(), anything())).thenResolve([]);
const updateRfqmJobCalledArgs: RfqmJobEntity[] = [];
when(mockDbUtils.updateRfqmJobAsync(anything())).thenCall(async (jobArg) => {
updateRfqmJobCalledArgs.push(_.cloneDeep(jobArg));
});
when(
mockDbUtils.findV2TransactionSubmissionsByOrderHashAsync(
anything(),
deepEqual([RfqmTransactionSubmissionType.ApprovalAndTrade]),
),
).thenResolve([]);
when(mockDbUtils.findV2TransactionSubmissionsByOrderHashAsync(anything())).thenResolve([]);
when(mockDbUtils.findV2TransactionSubmissionByTransactionHashAsync('0xsignedtransactionhash')).thenResolve(
_.cloneDeep(mockTransaction),
);
const mockQuoteServerClient = mock(QuoteServerClient);
when(mockQuoteServerClient.signV2Async(anything(), anything(), anything())).thenResolve(validEIP712Sig);
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.getExchangeProxyAddress()).thenReturn('0xexchangeproxyaddress');
when(mockBlockchainUtils.getTokenBalancesAsync(anything())).thenResolve([new BigNumber(1000000000)]);
when(
mockBlockchainUtils.generateTakerSignedOtcOrderCallData(
anything(),
anything(),
anything(),
anything(),
anything(),
),
).thenReturn('0xcalldata');
when(
mockBlockchainUtils.generatePermitAndCallCalldataAsync(anything(), anything(), anything(), anything()),
).thenResolve('0xcalldata');
when(mockBlockchainUtils.estimateGasForAsync(anything())).thenResolve(0);
when(mockBlockchainUtils.getNonceAsync('0xworkeraddress')).thenResolve(mockNonce);
when(mockBlockchainUtils.estimateGasForAsync(anything())).thenResolve(100);
when(
mockBlockchainUtils.transformTxDataToTransactionRequest(anything(), anything(), anything()),
).thenReturn(mockTransactionRequest);
when(mockBlockchainUtils.submitSignedTransactionAsync(anything())).thenResolve('0xsignedtransactionhash');
when(mockBlockchainUtils.signTransactionAsync(anything())).thenResolve({
signedTransaction: 'signedTransaction',
transactionHash: '0xsignedtransactionhash',
});
when(mockBlockchainUtils.getReceiptsAsync(deepEqual(['0xsignedtransactionhash']))).thenResolve([
mockTransactionReceipt,
]);
when(mockBlockchainUtils.getBlockAsync('0xblockhash')).thenResolve(mockMinedBlock);
when(mockBlockchainUtils.getCurrentBlockAsync()).thenResolve(4);
const mockRfqMakerBalanceCacheService = mock(RfqMakerBalanceCacheService);
when(mockRfqMakerBalanceCacheService.getERC20OwnerBalancesAsync(anything(), anything())).thenResolve([
new BigNumber(1000000000),
]);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
quoteServerClient: instance(mockQuoteServerClient),
rfqBlockchainUtils: instance(mockBlockchainUtils),
rfqMakerBalanceCacheService: instance(mockRfqMakerBalanceCacheService),
});
await rfqmService.processAtomicApprovalAndTradeAsync(job, '0xworkeraddress');
expect(updateRfqmJobCalledArgs[0].status).to.equal(RfqmJobStatus.PendingProcessing);
expect(updateRfqmJobCalledArgs[1].status).to.equal(RfqmJobStatus.PendingLastLookAccepted);
expect(updateRfqmJobCalledArgs[2].status).to.equal(RfqmJobStatus.PendingSubmitted);
expect(updateRfqmJobCalledArgs[updateRfqmJobCalledArgs.length - 1].status).to.equal(
RfqmJobStatus.SucceededConfirmed,
);
expect(job.status).to.equal(RfqmJobStatus.SucceededConfirmed);
verify(
mockBlockchainUtils.generatePermitAndCallCalldataAsync(anything(), anything(), anything(), anything()),
).once();
});
it('should process an atomic approval and meta-transaction v1 job trade successfully', async () => {
const nowS = Math.round(new Date().getTime() / ONE_SECOND_MS);
const jobId = 'jobId';
const transactionSubmissionId = 'submissionId';
const job = createMetaTransactionJobEntity(
{
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(nowS + 600),
fee: {
amount: new BigNumber(0),
token: '',
type: 'fixed',
},
inputToken: '0xinputToken',
inputTokenAmount: new BigNumber(10),
integratorId: '0xintegrator',
metaTransaction: MOCK_META_TRANSACTION,
metaTransactionHash: MOCK_META_TRANSACTION.getHash(),
minOutputTokenAmount: new BigNumber(10),
outputToken: '0xoutputToken',
takerAddress: '0xtakerAddress',
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
approval: MOCK_PERMIT_APPROVAL,
approvalSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
status: RfqmJobStatus.PendingEnqueued,
workerAddress: '0xworkeraddress',
},
jobId,
);
const mockTransactionRequest: providers.TransactionRequest = {};
const mockTransaction = createMetaTransactionSubmissionEntity(
{
from: '0xworkeraddress',
metaTransactionJobId: jobId,
maxFeePerGas: new BigNumber(100000),
maxPriorityFeePerGas: new BigNumber(100),
nonce: 0,
to: '0xexchangeproxyaddress',
transactionHash: '0xsignedtransactionhash',
type: RfqmTransactionSubmissionType.Trade,
},
transactionSubmissionId,
);
const mockTransactionReceipt: providers.TransactionReceipt = {
blockHash: '0xblockhash',
blockNumber: 1,
byzantium: true,
confirmations: 3,
contractAddress: '0xexchangeproxyaddress',
cumulativeGasUsed: EthersBigNumber.from(1000),
effectiveGasPrice: EthersBigNumber.from(1000),
from: '0xworkeraddress',
gasUsed: EthersBigNumber.from(10000),
logs: [],
logsBloom: '',
status: 1,
to: '0xexchangeproxyaddress',
transactionHash: '0xsignedtransactionhash',
transactionIndex: 0,
type: 2,
};
const mockMinedBlock: providers.Block = {
_difficulty: EthersBigNumber.from(2),
difficulty: 2,
extraData: '',
gasLimit: EthersBigNumber.from(1000),
gasUsed: EthersBigNumber.from(1000),
hash: '0xblockhash',
miner: '0xminer',
nonce: '0x000',
number: 21,
parentHash: '0xparentblockhash',
timestamp: 12345,
transactions: ['0xpresubmittransactionhash'],
};
const mockNonce = 0;
const gasStationAttendantMock = mock(GasStationAttendantEthereum);
when(gasStationAttendantMock.getExpectedTransactionGasRateAsync()).thenResolve(
new BigNumber(10).shiftedBy(GWEI_DECIMALS),
);
const mockDbUtils = mock(RfqmDbUtils);
when(mockDbUtils.findMetaTransactionSubmissionsByJobIdAsync(jobId)).thenResolve([]);
const updateRfqmJobCalledArgs: MetaTransactionJobEntity[] = [];
when(mockDbUtils.updateRfqmJobAsync(anything())).thenCall(async (jobArg) => {
updateRfqmJobCalledArgs.push(_.cloneDeep(jobArg));
});
when(
mockDbUtils.findMetaTransactionSubmissionsByJobIdAsync(
jobId,
deepEqual([RfqmTransactionSubmissionType.ApprovalAndTrade]),
),
).thenResolve([]);
when(
mockDbUtils.findMetaTransactionSubmissionsByTransactionHashAsync(
'0xsignedtransactionhash',
RfqmTransactionSubmissionType.ApprovalAndTrade,
),
).thenResolve([_.cloneDeep(mockTransaction)]);
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.getExchangeProxyAddress()).thenReturn('0xexchangeproxyaddress');
when(
mockBlockchainUtils.generateMetaTransactionCallData(anything(), 'v2', anything(), anything()),
).thenReturn('0xcalldata');
when(
mockBlockchainUtils.generatePermitAndCallCalldataAsync(anything(), anything(), anything(), anything()),
).thenResolve('0xcalldata');
when(mockBlockchainUtils.getPermitAndCallAddress()).thenReturn('0xpermitandcalladdress');
when(mockBlockchainUtils.estimateGasForAsync(anything())).thenResolve(0);
when(mockBlockchainUtils.getNonceAsync('0xworkeraddress')).thenResolve(mockNonce);
when(
mockBlockchainUtils.transformTxDataToTransactionRequest(anything(), anything(), anything()),
).thenReturn(mockTransactionRequest);
when(mockBlockchainUtils.submitSignedTransactionAsync(anything())).thenResolve('0xsignedtransactionhash');
when(mockBlockchainUtils.signTransactionAsync(anything())).thenResolve({
signedTransaction: 'signedTransaction',
transactionHash: '0xsignedtransactionhash',
});
when(mockBlockchainUtils.getReceiptsAsync(deepEqual(['0xsignedtransactionhash']))).thenResolve([
mockTransactionReceipt,
]);
when(mockBlockchainUtils.getBlockAsync('0xblockhash')).thenResolve(mockMinedBlock);
when(mockBlockchainUtils.getCurrentBlockAsync()).thenResolve(4);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
rfqBlockchainUtils: instance(mockBlockchainUtils),
});
await rfqmService.processAtomicApprovalAndTradeAsync(job, '0xworkeraddress');
expect(updateRfqmJobCalledArgs[0].status).to.equal(RfqmJobStatus.PendingProcessing);
expect(updateRfqmJobCalledArgs[1].status).to.equal(RfqmJobStatus.PendingSubmitted);
expect(updateRfqmJobCalledArgs[updateRfqmJobCalledArgs.length - 1].status).to.equal(
RfqmJobStatus.SucceededConfirmed,
);
expect(job.status).to.equal(RfqmJobStatus.SucceededConfirmed);
verify(
mockBlockchainUtils.generatePermitAndCallCalldataAsync(anything(), anything(), anything(), anything()),
).once();
});
it('should process an atomic approval and meta-transaction v2 job trade successfully', async () => {
const nowS = Math.round(new Date().getTime() / ONE_SECOND_MS);
const jobId = 'jobId';
const transactionSubmissionId = 'submissionId';
const job = createMetaTransactionV2JobEntity(
{
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(nowS + 600),
inputToken: '0xinputToken',
inputTokenAmount: new BigNumber(10),
integratorId: '0xintegrator',
calledFunction: 'transformERC20',
metaTransaction: MOCK_META_TRANSACTION_V2,
metaTransactionHash: MOCK_META_TRANSACTION_V2.getHash(),
minOutputTokenAmount: new BigNumber(10),
outputToken: '0xoutputToken',
takerAddress: '0xtakerAddress',
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
approval: MOCK_EXECUTE_META_TRANSACTION_APPROVAL,
approvalSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
tokens: ['0xinputToken', '0xoutputToken'],
status: RfqmJobStatus.PendingEnqueued,
workerAddress: '0xworkeraddress',
},
jobId,
);
const mockTransactionRequest: providers.TransactionRequest = {};
const mockTransaction = createMetaTransactionV2SubmissionEntity(
{
from: '0xworkeraddress',
metaTransactionV2JobId: jobId,
maxFeePerGas: new BigNumber(100000),
maxPriorityFeePerGas: new BigNumber(100),
nonce: 0,
to: '0xexchangeproxyaddress',
transactionHash: '0xsignedtransactionhash',
type: RfqmTransactionSubmissionType.Trade,
status: RfqmTransactionSubmissionStatus.Submitted,
},
transactionSubmissionId,
);
const mockTransactionReceipt: providers.TransactionReceipt = {
blockHash: '0xblockhash',
blockNumber: 1,
byzantium: true,
confirmations: 3,
contractAddress: '0xexchangeproxyaddress',
cumulativeGasUsed: EthersBigNumber.from(1000),
effectiveGasPrice: EthersBigNumber.from(1000),
from: '0xworkeraddress',
gasUsed: EthersBigNumber.from(10000),
logs: [],
logsBloom: '',
status: 1,
to: '0xexchangeproxyaddress',
transactionHash: '0xsignedtransactionhash',
transactionIndex: 0,
type: 2,
};
const mockMinedBlock: providers.Block = {
_difficulty: EthersBigNumber.from(2),
difficulty: 2,
extraData: '',
gasLimit: EthersBigNumber.from(1000),
gasUsed: EthersBigNumber.from(1000),
hash: '0xblockhash',
miner: '0xminer',
nonce: '0x000',
number: 21,
parentHash: '0xparentblockhash',
timestamp: 12345,
transactions: ['0xpresubmittransactionhash'],
};
const mockNonce = 0;
const gasStationAttendantMock = mock(GasStationAttendantEthereum);
when(gasStationAttendantMock.getExpectedTransactionGasRateAsync()).thenResolve(
new BigNumber(10).shiftedBy(GWEI_DECIMALS),
);
const mockDbUtils = mock(RfqmDbUtils);
when(mockDbUtils.findMetaTransactionV2SubmissionsByJobIdAsync(jobId)).thenResolve([]);
const updateRfqmJobCalledArgs: MetaTransactionJobEntity[] = [];
when(mockDbUtils.updateRfqmJobAsync(anything())).thenCall(async (jobArg) => {
updateRfqmJobCalledArgs.push(_.cloneDeep(jobArg));
});
when(
mockDbUtils.findMetaTransactionV2SubmissionsByJobIdAsync(
jobId,
deepEqual([RfqmTransactionSubmissionType.ApprovalAndTrade]),
),
).thenResolve([]);
when(
mockDbUtils.findMetaTransactionV2SubmissionsByTransactionHashAsync(
'0xsignedtransactionhash',
RfqmTransactionSubmissionType.ApprovalAndTrade,
),
).thenResolve([_.cloneDeep(mockTransaction)]);
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.getExchangeProxyAddress()).thenReturn('0xexchangeproxyaddress');
when(
mockBlockchainUtils.generateMetaTransactionCallData(anything(), 'v2', anything(), anything()),
).thenReturn('0xcalldata');
when(
mockBlockchainUtils.generatePermitAndCallCalldataAsync(anything(), anything(), anything(), anything()),
).thenResolve('0xcalldata');
when(mockBlockchainUtils.getPermitAndCallAddress()).thenReturn('0xpermitandcalladdress');
when(mockBlockchainUtils.estimateGasForAsync(anything())).thenResolve(0);
when(mockBlockchainUtils.getNonceAsync('0xworkeraddress')).thenResolve(mockNonce);
when(
mockBlockchainUtils.transformTxDataToTransactionRequest(anything(), anything(), anything()),
).thenReturn(mockTransactionRequest);
when(mockBlockchainUtils.submitSignedTransactionAsync(anything())).thenResolve('0xsignedtransactionhash');
when(mockBlockchainUtils.signTransactionAsync(anything())).thenResolve({
signedTransaction: 'signedTransaction',
transactionHash: '0xsignedtransactionhash',
});
when(mockBlockchainUtils.getReceiptsAsync(deepEqual(['0xsignedtransactionhash']))).thenResolve([
mockTransactionReceipt,
]);
when(mockBlockchainUtils.getBlockAsync('0xblockhash')).thenResolve(mockMinedBlock);
when(mockBlockchainUtils.getCurrentBlockAsync()).thenResolve(4);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
rfqBlockchainUtils: instance(mockBlockchainUtils),
});
await rfqmService.processAtomicApprovalAndTradeAsync(job, '0xworkeraddress');
expect(updateRfqmJobCalledArgs[0].status).to.equal(RfqmJobStatus.PendingProcessing);
expect(updateRfqmJobCalledArgs[1].status).to.equal(RfqmJobStatus.PendingSubmitted);
expect(updateRfqmJobCalledArgs[updateRfqmJobCalledArgs.length - 1].status).to.equal(
RfqmJobStatus.SucceededConfirmed,
);
expect(job.status).to.equal(RfqmJobStatus.SucceededConfirmed);
verify(
mockBlockchainUtils.generatePermitAndCallCalldataAsync(anything(), anything(), anything(), anything()),
).once();
});
});
describe('processApprovalAndTradeAsync', () => {
it('throws if non-approval job is supplied to the method for a rfqm v2 job', async () => {
const job = new RfqmV2JobEntity({
affiliateAddress: '',
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
fee: {
amount: '0',
token: '',
type: 'fixed',
},
integratorId: '',
lastLookResult: null,
makerUri: 'http://foo.bar',
makerSignature: null,
order: {
order: {
chainId: '1',
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
new BigNumber(fakeFiveMinutesLater.toString()),
new BigNumber(1),
new BigNumber(1),
).toString(),
maker: '0xmaker',
makerAmount: '1000000',
makerToken: '0xmakertoken',
taker: '0xtaker',
takerAmount: '10000000',
takerToken: '0xtakertoken',
txOrigin: '',
verifyingContract: '',
},
type: RfqmOrderTypes.Otc,
},
orderHash: '0x01234567',
status: RfqmJobStatus.PendingEnqueued,
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
updatedAt: new Date(),
workerAddress: '',
workflow: 'rfqm',
});
const rfqmService = buildWorkerServiceForUnitTest();
try {
await rfqmService.processApprovalAndTradeAsync(job, '0xworkeraddress');
expect.fail();
} catch (e) {
expect(e.message).to.contain(
'Non-approval job should not be processed by `processApprovalAndTradeAsync`',
);
}
});
it('should not proceed to trade transaction if the status of approval transaction is not `SucceededConfirmed` for a rfqm v2 job', async () => {
const nowS = Math.round(new Date().getTime() / ONE_SECOND_MS);
const job = new RfqmV2JobEntity({
affiliateAddress: '',
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(nowS + 10),
fee: {
amount: '0',
token: '',
type: 'fixed',
},
integratorId: '',
lastLookResult: null,
makerUri: 'http://foo.bar',
makerSignature: null,
order: {
order: {
chainId: '1',
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
new BigNumber(nowS + 10),
new BigNumber(1),
new BigNumber(1),
).toString(),
maker,
makerAmount: '1000000',
makerToken: '0xmakertoken',
taker: '0xtaker',
takerAmount: '10000000',
takerToken: '0xtakertoken',
txOrigin: '',
verifyingContract: '',
},
type: RfqmOrderTypes.Otc,
},
orderHash,
status: RfqmJobStatus.PendingEnqueued,
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
updatedAt: new Date(),
workerAddress: '',
workflow: 'rfqm',
approval: MOCK_EXECUTE_META_TRANSACTION_APPROVAL,
approvalSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
});
const mockPresubmitTransaction = new RfqmV2TransactionSubmissionEntity({
createdAt: new Date(1233),
from: '0xworkeraddress',
maxFeePerGas: new BigNumber(100000),
maxPriorityFeePerGas: new BigNumber(100),
nonce: 0,
orderHash: '0x01234567',
status: RfqmTransactionSubmissionStatus.Submitted,
to: '0xexchangeproxyaddress',
transactionHash: '0xpresubmittransactionhash',
type: RfqmTransactionSubmissionType.Approval,
});
const mockTransactionReceipt: providers.TransactionReceipt = {
blockHash: '0xblockhash',
blockNumber: 1,
byzantium: true,
confirmations: 3,
contractAddress: '0xexchangeproxyaddress',
cumulativeGasUsed: EthersBigNumber.from(1000),
effectiveGasPrice: EthersBigNumber.from(1000),
from: '0xworkeraddress',
gasUsed: EthersBigNumber.from(10000),
logs: [],
logsBloom: '',
status: 0,
to: '0xexchangeproxyaddress',
transactionHash: '0xpresubmittransactionhash',
transactionIndex: 0,
type: 2,
};
const mockMinedBlock: providers.Block = {
_difficulty: EthersBigNumber.from(2),
difficulty: 2,
extraData: '',
gasLimit: EthersBigNumber.from(1000),
gasUsed: EthersBigNumber.from(1000),
hash: '0xblockhash',
miner: '0xminer',
nonce: '0x000',
number: 21,
parentHash: '0xparentblockhash',
timestamp: 12345,
transactions: ['0xpresubmittransactionhash'],
};
const mockNonce = 0;
const gasStationAttendantMock = mock(GasStationAttendantEthereum);
when(gasStationAttendantMock.getExpectedTransactionGasRateAsync()).thenResolve(
new BigNumber(10).shiftedBy(GWEI_DECIMALS),
);
const mockDbUtils = mock(RfqmDbUtils);
// when(mockDbUtils.findV2TransactionSubmissionsByOrderHashAsync(anything())).thenResolve([]);
when(
mockDbUtils.findV2TransactionSubmissionsByOrderHashAsync(
anything(),
deepEqual([RfqmTransactionSubmissionType.Approval]),
),
).thenResolve([mockPresubmitTransaction]);
const updateRfqmJobCalledArgs: RfqmJobEntity[] = [];
when(mockDbUtils.updateRfqmJobAsync(anything())).thenCall(async (jobArg) => {
updateRfqmJobCalledArgs.push(_.cloneDeep(jobArg));
});
const mockQuoteServerClient = mock(QuoteServerClient);
when(mockQuoteServerClient.signV2Async(anything(), anything(), anything())).thenResolve(validEIP712Sig);
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.isValidOrderSignerAsync(anything(), anything())).thenResolve(true);
when(mockBlockchainUtils.getMinOfBalancesAndAllowancesAsync(anything())).thenResolve([
new BigNumber(1000000000),
]);
when(mockBlockchainUtils.getTokenBalancesAsync(anything())).thenResolve([new BigNumber(1000000000)]);
when(mockBlockchainUtils.generateApprovalCalldataAsync(anything(), anything(), anything())).thenResolve(
'0xcalldata',
);
when(mockBlockchainUtils.getNonceAsync('0xworkeraddress')).thenResolve(mockNonce);
when(mockBlockchainUtils.getReceiptsAsync(deepEqual(['0xpresubmittransactionhash']))).thenResolve([
mockTransactionReceipt,
]);
when(mockBlockchainUtils.getBlockAsync('0xblockhash')).thenResolve(mockMinedBlock);
when(mockBlockchainUtils.getCurrentBlockAsync()).thenResolve(4);
const mockRfqMakerBalanceCacheService = mock(RfqMakerBalanceCacheService);
when(mockRfqMakerBalanceCacheService.getERC20OwnerBalancesAsync(anything(), anything())).thenResolve([
new BigNumber(1000000000),
]);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
quoteServerClient: instance(mockQuoteServerClient),
rfqBlockchainUtils: instance(mockBlockchainUtils),
rfqMakerBalanceCacheService: instance(mockRfqMakerBalanceCacheService),
});
await rfqmService.processApprovalAndTradeAsync(job, '0xworkeraddress');
expect(updateRfqmJobCalledArgs[0].status).to.equal(RfqmJobStatus.PendingProcessing);
expect(updateRfqmJobCalledArgs[1].status).to.equal(RfqmJobStatus.PendingLastLookAccepted);
expect(updateRfqmJobCalledArgs[updateRfqmJobCalledArgs.length - 1].status).to.equal(
RfqmJobStatus.FailedRevertedConfirmed,
);
expect(job.status).to.equal(RfqmJobStatus.FailedRevertedConfirmed);
});
it('should proceed to trade transaction if the status of approval transaction is `SucceededConfirmed` for a rfqm v2 job', async () => {
const nowS = Math.round(new Date().getTime() / ONE_SECOND_MS);
const job = new RfqmV2JobEntity({
affiliateAddress: '',
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(nowS + 10),
fee: {
amount: '0',
token: '',
type: 'fixed',
},
integratorId: '',
lastLookResult: null,
makerUri: 'http://foo.bar',
makerSignature: null,
order: {
order: {
chainId: '1',
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
new BigNumber(nowS + 10),
new BigNumber(1),
new BigNumber(1),
).toString(),
maker,
makerAmount: '1000000',
makerToken: '0xmakertoken',
taker: '0xtaker',
takerAmount: '10000000',
takerToken: '0xtakertoken',
txOrigin: '',
verifyingContract: '',
},
type: RfqmOrderTypes.Otc,
},
orderHash,
status: RfqmJobStatus.PendingEnqueued,
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
updatedAt: new Date(),
workerAddress: '',
workflow: 'rfqm',
approval: MOCK_EXECUTE_META_TRANSACTION_APPROVAL,
approvalSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
});
const mockPresubmitApprovalTransaction = new RfqmV2TransactionSubmissionEntity({
createdAt: new Date(1233),
from: '0xworkeraddress',
maxFeePerGas: new BigNumber(100000),
maxPriorityFeePerGas: new BigNumber(100),
nonce: 0,
orderHash: '0x01234567',
status: RfqmTransactionSubmissionStatus.Submitted,
to: '0xexchangeproxyaddress',
transactionHash: '0xpresubmittransactionhash',
type: RfqmTransactionSubmissionType.Approval,
});
const mockPresubmitTradeTransaction = new RfqmV2TransactionSubmissionEntity({
createdAt: new Date(1233),
from: '0xworkeraddress',
maxFeePerGas: new BigNumber(100000),
maxPriorityFeePerGas: new BigNumber(100),
nonce: 0,
orderHash: '0x01234567',
status: RfqmTransactionSubmissionStatus.Submitted,
to: '0xexchangeproxyaddress',
transactionHash: '0xpresubmittransactionhash',
type: RfqmTransactionSubmissionType.Trade,
});
const mockTransactionReceipt: providers.TransactionReceipt = {
blockHash: '0xblockhash',
blockNumber: 1,
byzantium: true,
confirmations: 3,
contractAddress: '0xexchangeproxyaddress',
cumulativeGasUsed: EthersBigNumber.from(1000),
effectiveGasPrice: EthersBigNumber.from(1000),
from: '0xworkeraddress',
gasUsed: EthersBigNumber.from(10000),
logs: [],
logsBloom: '',
status: 1,
to: '0xexchangeproxyaddress',
transactionHash: '0xpresubmittransactionhash',
transactionIndex: 0,
type: 2,
};
const mockMinedBlock: providers.Block = {
_difficulty: EthersBigNumber.from(2),
difficulty: 2,
extraData: '',
gasLimit: EthersBigNumber.from(1000),
gasUsed: EthersBigNumber.from(1000),
hash: '0xblockhash',
miner: '0xminer',
nonce: '0x000',
number: 21,
parentHash: '0xparentblockhash',
timestamp: 12345,
transactions: ['0xpresubmittransactionhash'],
};
const mockNonce = 0;
const gasStationAttendantMock = mock(GasStationAttendantEthereum);
when(gasStationAttendantMock.getExpectedTransactionGasRateAsync()).thenResolve(
new BigNumber(10).shiftedBy(GWEI_DECIMALS),
);
const mockDbUtils = mock(RfqmDbUtils);
// when(mockDbUtils.findV2TransactionSubmissionsByOrderHashAsync(anything())).thenResolve([]);
when(
mockDbUtils.findV2TransactionSubmissionsByOrderHashAsync(
anything(),
deepEqual([RfqmTransactionSubmissionType.Approval]),
),
).thenResolve([mockPresubmitApprovalTransaction]);
when(
mockDbUtils.findV2TransactionSubmissionsByOrderHashAsync(
anything(),
deepEqual([RfqmTransactionSubmissionType.Trade]),
),
).thenResolve([mockPresubmitTradeTransaction]);
when(mockDbUtils.findV2TransactionSubmissionsByOrderHashAsync(anything())).thenResolve([
mockPresubmitTradeTransaction,
]);
const updateRfqmJobCalledArgs: RfqmJobEntity[] = [];
when(mockDbUtils.updateRfqmJobAsync(anything())).thenCall(async (jobArg) => {
updateRfqmJobCalledArgs.push(_.cloneDeep(jobArg));
});
const mockQuoteServerClient = mock(QuoteServerClient);
when(mockQuoteServerClient.signV2Async(anything(), anything(), anything())).thenResolve(validEIP712Sig);
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.isValidOrderSignerAsync(anything(), anything())).thenResolve(true);
when(mockBlockchainUtils.getMinOfBalancesAndAllowancesAsync(anything())).thenResolve([
new BigNumber(1000000000),
]);
when(mockBlockchainUtils.getTokenBalancesAsync(anything())).thenResolve([new BigNumber(1000000000)]);
when(mockBlockchainUtils.estimateGasForAsync(anything())).thenResolve(0);
when(
mockBlockchainUtils.generateTakerSignedOtcOrderCallData(
anything(),
anything(),
anything(),
anything(),
anything(),
),
).thenReturn('0xcalldata');
when(mockBlockchainUtils.generateApprovalCalldataAsync(anything(), anything(), anything())).thenResolve(
'0xcalldata',
);
when(mockBlockchainUtils.getNonceAsync('0xworkeraddress')).thenResolve(mockNonce);
when(mockBlockchainUtils.getReceiptsAsync(deepEqual(['0xpresubmittransactionhash']))).thenResolve([
mockTransactionReceipt,
]);
when(mockBlockchainUtils.getBlockAsync('0xblockhash')).thenResolve(mockMinedBlock);
when(mockBlockchainUtils.getCurrentBlockAsync()).thenResolve(4);
const mockRfqMakerBalanceCacheService = mock(RfqMakerBalanceCacheService);
when(mockRfqMakerBalanceCacheService.getERC20OwnerBalancesAsync(anything(), anything())).thenResolve([
new BigNumber(1000000000),
]);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
quoteServerClient: instance(mockQuoteServerClient),
rfqBlockchainUtils: instance(mockBlockchainUtils),
rfqMakerBalanceCacheService: instance(mockRfqMakerBalanceCacheService),
});
await rfqmService.processApprovalAndTradeAsync(job, '0xworkeraddress');
expect(updateRfqmJobCalledArgs[0].status).to.equal(RfqmJobStatus.PendingProcessing);
expect(updateRfqmJobCalledArgs[1].status).to.equal(RfqmJobStatus.PendingLastLookAccepted);
expect(updateRfqmJobCalledArgs[2].status).to.equal(RfqmJobStatus.PendingSubmitted);
expect(updateRfqmJobCalledArgs[updateRfqmJobCalledArgs.length - 1].status).to.equal(
RfqmJobStatus.SucceededConfirmed,
);
expect(job.status).to.equal(RfqmJobStatus.SucceededConfirmed);
});
it('throws if non-approval job is supplied to the method for a meta-transaction job', async () => {
const jobId = 'jobId';
const job = createMetaTransactionJobEntity(
{
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
fee: {
amount: new BigNumber(0),
token: '',
type: 'fixed',
},
inputToken: '0xinputToken',
inputTokenAmount: new BigNumber(10),
integratorId: '0xintegrator',
metaTransaction: MOCK_META_TRANSACTION,
metaTransactionHash: MOCK_META_TRANSACTION.getHash(),
minOutputTokenAmount: new BigNumber(10),
outputToken: '0xoutputToken',
takerAddress: '0xtakerAddress',
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
status: RfqmJobStatus.PendingEnqueued,
workerAddress: '0xworkeraddress',
},
jobId,
);
const rfqmService = buildWorkerServiceForUnitTest();
try {
await rfqmService.processApprovalAndTradeAsync(job, '0xworkeraddress');
expect.fail();
} catch (e) {
expect(e.message).to.contain(
'Non-approval job should not be processed by `processApprovalAndTradeAsync`',
);
}
});
it('should not proceed to trade transaction if the status of approval transaction is not `SucceededConfirmed` for a meta-transaction job', async () => {
const nowS = Math.round(new Date().getTime() / ONE_SECOND_MS);
const jobId = 'jobId';
const transactionSubmissionId = 'submissionId';
const inputToken = '0xinputToken';
const job = createMetaTransactionJobEntity(
{
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(nowS + 600),
fee: {
amount: new BigNumber(0),
token: '',
type: 'fixed',
},
inputToken,
inputTokenAmount: new BigNumber(10),
integratorId: '0xintegrator',
metaTransaction: MOCK_META_TRANSACTION,
metaTransactionHash: MOCK_META_TRANSACTION.getHash(),
minOutputTokenAmount: new BigNumber(10),
outputToken: '0xoutputToken',
takerAddress: '0xtakerAddress',
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
status: RfqmJobStatus.PendingEnqueued,
workerAddress: '0xworkeraddress',
approval: MOCK_EXECUTE_META_TRANSACTION_APPROVAL,
approvalSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
},
jobId,
);
const mockTransaction = createMetaTransactionSubmissionEntity(
{
from: '0xworkeraddress',
metaTransactionJobId: jobId,
maxFeePerGas: new BigNumber(100000),
maxPriorityFeePerGas: new BigNumber(100),
nonce: 0,
to: '0xexchangeproxyaddress',
transactionHash: '0xsignedtransactionhash',
type: RfqmTransactionSubmissionType.Approval,
status: RfqmTransactionSubmissionStatus.Submitted,
},
transactionSubmissionId,
);
const mockTransactionReceipt: providers.TransactionReceipt = {
blockHash: '0xblockhash',
blockNumber: 1,
byzantium: true,
confirmations: 3,
contractAddress: '0xexchangeproxyaddress',
cumulativeGasUsed: EthersBigNumber.from(1000),
effectiveGasPrice: EthersBigNumber.from(1000),
from: '0xworkeraddress',
gasUsed: EthersBigNumber.from(10000),
logs: [],
logsBloom: '',
status: 0,
to: '0xexchangeproxyaddress',
transactionHash: '0xsignedtransactionhash',
transactionIndex: 0,
type: 2,
};
const mockMinedBlock: providers.Block = {
_difficulty: EthersBigNumber.from(2),
difficulty: 2,
extraData: '',
gasLimit: EthersBigNumber.from(1000),
gasUsed: EthersBigNumber.from(1000),
hash: '0xblockhash',
miner: '0xminer',
nonce: '0x000',
number: 21,
parentHash: '0xparentblockhash',
timestamp: 12345,
transactions: ['0xsignedtransactionhash'],
};
const mockNonce = 0;
const gasStationAttendantMock = mock(GasStationAttendantEthereum);
when(gasStationAttendantMock.getExpectedTransactionGasRateAsync()).thenResolve(
new BigNumber(10).shiftedBy(GWEI_DECIMALS),
);
const mockDbUtils = mock(RfqmDbUtils);
when(
mockDbUtils.findMetaTransactionSubmissionsByJobIdAsync(
jobId,
deepEqual([RfqmTransactionSubmissionType.Approval]),
),
).thenResolve([mockTransaction]);
const updateRfqmJobCalledArgs: MetaTransactionJobEntity[] = [];
when(mockDbUtils.updateRfqmJobAsync(anything())).thenCall(async (jobArg) => {
updateRfqmJobCalledArgs.push(_.cloneDeep(jobArg));
});
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.isValidOrderSignerAsync(anything(), anything())).thenResolve(true);
when(mockBlockchainUtils.getMinOfBalancesAndAllowancesAsync(anything())).thenResolve([
new BigNumber(1000000000),
]);
when(mockBlockchainUtils.getTokenBalancesAsync(anything())).thenResolve([new BigNumber(1000000000)]);
when(mockBlockchainUtils.generateApprovalCalldataAsync(inputToken, anything(), anything())).thenResolve(
'0xcalldata',
);
when(mockBlockchainUtils.getNonceAsync('0xworkeraddress')).thenResolve(mockNonce);
when(mockBlockchainUtils.getReceiptsAsync(deepEqual(['0xsignedtransactionhash']))).thenResolve([
mockTransactionReceipt,
]);
when(mockBlockchainUtils.getBlockAsync('0xblockhash')).thenResolve(mockMinedBlock);
when(mockBlockchainUtils.getCurrentBlockAsync()).thenResolve(4);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
rfqBlockchainUtils: instance(mockBlockchainUtils),
});
await rfqmService.processApprovalAndTradeAsync(job, '0xworkeraddress');
expect(updateRfqmJobCalledArgs[0].status).to.equal(RfqmJobStatus.PendingProcessing);
expect(updateRfqmJobCalledArgs[updateRfqmJobCalledArgs.length - 1].status).to.equal(
RfqmJobStatus.FailedRevertedConfirmed,
);
expect(job.status).to.equal(RfqmJobStatus.FailedRevertedConfirmed);
});
it('should proceed to trade transaction if the status of approval transaction is `SucceededConfirmed` for a meta-transaction job', async () => {
const nowS = Math.round(new Date().getTime() / ONE_SECOND_MS);
const jobId = 'jobId';
const transactionSubmissionId1 = 'submissionId1';
const transactionSubmissionId2 = 'submissionId2';
const inputToken = '0xinputToken';
const job = createMetaTransactionJobEntity(
{
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(nowS + 600),
fee: {
amount: new BigNumber(0),
token: '',
type: 'fixed',
},
inputToken,
inputTokenAmount: new BigNumber(10),
integratorId: '0xintegrator',
metaTransaction: MOCK_META_TRANSACTION,
metaTransactionHash: MOCK_META_TRANSACTION.getHash(),
minOutputTokenAmount: new BigNumber(10),
outputToken: '0xoutputToken',
takerAddress: '0xtakerAddress',
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
status: RfqmJobStatus.PendingEnqueued,
workerAddress: '0xworkeraddress',
approval: MOCK_EXECUTE_META_TRANSACTION_APPROVAL,
approvalSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
},
jobId,
);
const mockApprovalTransaction = createMetaTransactionSubmissionEntity(
{
from: '0xworkeraddress',
metaTransactionJobId: jobId,
maxFeePerGas: new BigNumber(100000),
maxPriorityFeePerGas: new BigNumber(100),
nonce: 0,
to: '0xexchangeproxyaddress',
transactionHash: '0xsignedtransactionhash1',
type: RfqmTransactionSubmissionType.Approval,
status: RfqmTransactionSubmissionStatus.Submitted,
},
transactionSubmissionId1,
);
const mockTradeTransaction = createMetaTransactionSubmissionEntity(
{
from: '0xworkeraddress',
metaTransactionJobId: jobId,
maxFeePerGas: new BigNumber(100000),
maxPriorityFeePerGas: new BigNumber(100),
nonce: 0,
to: '0xexchangeproxyaddress',
transactionHash: '0xsignedtransactionhash2',
type: RfqmTransactionSubmissionType.Trade,
status: RfqmTransactionSubmissionStatus.Submitted,
},
transactionSubmissionId2,
);
const mockApprovalTransactionReceipt: providers.TransactionReceipt = {
blockHash: '0xblockhash',
blockNumber: 1,
byzantium: true,
confirmations: 3,
contractAddress: '0xexchangeproxyaddress',
cumulativeGasUsed: EthersBigNumber.from(1000),
effectiveGasPrice: EthersBigNumber.from(1000),
from: '0xworkeraddress',
gasUsed: EthersBigNumber.from(10000),
logs: [],
logsBloom: '',
status: 1,
to: '0xexchangeproxyaddress',
transactionHash: '0xsignedtransactionhash1',
transactionIndex: 0,
type: 2,
};
const mockTradeTransactionReceipt: providers.TransactionReceipt = {
blockHash: '0xblockhash',
blockNumber: 1,
byzantium: true,
confirmations: 3,
contractAddress: '0xexchangeproxyaddress',
cumulativeGasUsed: EthersBigNumber.from(1000),
effectiveGasPrice: EthersBigNumber.from(1000),
from: '0xworkeraddress',
gasUsed: EthersBigNumber.from(10000),
logs: [],
logsBloom: '',
status: 1,
to: '0xexchangeproxyaddress',
transactionHash: '0xsignedtransactionhash2',
transactionIndex: 0,
type: 2,
};
const mockMinedBlock: providers.Block = {
_difficulty: EthersBigNumber.from(2),
difficulty: 2,
extraData: '',
gasLimit: EthersBigNumber.from(1000),
gasUsed: EthersBigNumber.from(1000),
hash: '0xblockhash',
miner: '0xminer',
nonce: '0x000',
number: 21,
parentHash: '0xparentblockhash',
timestamp: 12345,
transactions: ['0xsignedtransactionhash1', '0xsignedtransactionhash2'],
};
const mockNonce = 0;
const gasStationAttendantMock = mock(GasStationAttendantEthereum);
when(gasStationAttendantMock.getExpectedTransactionGasRateAsync()).thenResolve(
new BigNumber(10).shiftedBy(GWEI_DECIMALS),
);
const mockDbUtils = mock(RfqmDbUtils);
when(
mockDbUtils.findMetaTransactionSubmissionsByJobIdAsync(
jobId,
deepEqual([RfqmTransactionSubmissionType.Approval]),
),
).thenResolve([mockApprovalTransaction]);
when(
mockDbUtils.findMetaTransactionSubmissionsByJobIdAsync(
jobId,
deepEqual([RfqmTransactionSubmissionType.Trade]),
),
).thenResolve([mockTradeTransaction]);
when(mockDbUtils.findMetaTransactionSubmissionsByJobIdAsync(jobId)).thenResolve([mockTradeTransaction]);
const updateRfqmJobCalledArgs: MetaTransactionJobEntity[] = [];
when(mockDbUtils.updateRfqmJobAsync(anything())).thenCall(async (jobArg) => {
updateRfqmJobCalledArgs.push(_.cloneDeep(jobArg));
});
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.isValidOrderSignerAsync(anything(), anything())).thenResolve(true);
when(mockBlockchainUtils.getMinOfBalancesAndAllowancesAsync(anything())).thenResolve([
new BigNumber(1000000000),
]);
when(mockBlockchainUtils.getTokenBalancesAsync(anything())).thenResolve([new BigNumber(1000000000)]);
when(mockBlockchainUtils.estimateGasForAsync(anything())).thenResolve(0);
when(
mockBlockchainUtils.generateMetaTransactionCallData(anything(), 'v1', anything(), anything()),
).thenReturn('0xcalldata');
when(mockBlockchainUtils.generateApprovalCalldataAsync(anything(), anything(), anything())).thenResolve(
'0xcalldata',
);
when(mockBlockchainUtils.getNonceAsync('0xworkeraddress')).thenResolve(mockNonce);
when(mockBlockchainUtils.getReceiptsAsync(deepEqual(['0xsignedtransactionhash1']))).thenResolve([
mockApprovalTransactionReceipt,
]);
when(mockBlockchainUtils.getReceiptsAsync(deepEqual(['0xsignedtransactionhash2']))).thenResolve([
mockTradeTransactionReceipt,
]);
when(mockBlockchainUtils.getBlockAsync('0xblockhash')).thenResolve(mockMinedBlock);
when(mockBlockchainUtils.getCurrentBlockAsync()).thenResolve(4);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
rfqBlockchainUtils: instance(mockBlockchainUtils),
});
await rfqmService.processApprovalAndTradeAsync(job, '0xworkeraddress');
expect(updateRfqmJobCalledArgs[0].status).to.equal(RfqmJobStatus.PendingProcessing);
expect(updateRfqmJobCalledArgs[1].status).to.equal(RfqmJobStatus.PendingSubmitted);
expect(updateRfqmJobCalledArgs[updateRfqmJobCalledArgs.length - 1].status).to.equal(
RfqmJobStatus.SucceededConfirmed,
);
expect(job.status).to.equal(RfqmJobStatus.SucceededConfirmed);
});
it('throws if non-approval job is supplied to the method for a meta-transaction v2 job', async () => {
const jobId = 'jobId';
const job = createMetaTransactionV2JobEntity(
{
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
inputToken: '0xinputToken',
inputTokenAmount: new BigNumber(10),
integratorId: '0xintegrator',
calledFunction: 'transformERC20',
metaTransaction: MOCK_META_TRANSACTION_V2,
metaTransactionHash: MOCK_META_TRANSACTION_V2.getHash(),
minOutputTokenAmount: new BigNumber(10),
outputToken: '0xoutputToken',
takerAddress: '0xtakerAddress',
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
tokens: ['0xinputToken', '0xoutputToken'],
status: RfqmJobStatus.PendingEnqueued,
workerAddress: '0xworkeraddress',
},
jobId,
);
const rfqmService = buildWorkerServiceForUnitTest();
try {
await rfqmService.processApprovalAndTradeAsync(job, '0xworkeraddress');
expect.fail();
} catch (e) {
expect(e.message).to.contain(
'Non-approval job should not be processed by `processApprovalAndTradeAsync`',
);
}
});
it('should not proceed to trade transaction if the status of approval transaction is not `SucceededConfirmed` for a meta-transaction v2 job', async () => {
const nowS = Math.round(new Date().getTime() / ONE_SECOND_MS);
const jobId = 'jobId';
const transactionSubmissionId = 'submissionId';
const inputToken = '0xinputToken';
const job = createMetaTransactionV2JobEntity(
{
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(nowS + 600),
inputToken: '0xinputToken',
inputTokenAmount: new BigNumber(10),
integratorId: '0xintegrator',
calledFunction: 'transformERC20',
metaTransaction: MOCK_META_TRANSACTION_V2,
metaTransactionHash: MOCK_META_TRANSACTION_V2.getHash(),
minOutputTokenAmount: new BigNumber(10),
outputToken: '0xoutputToken',
takerAddress: '0xtakerAddress',
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
tokens: ['0xinputToken', '0xoutputToken'],
status: RfqmJobStatus.PendingEnqueued,
workerAddress: '0xworkeraddress',
approval: MOCK_EXECUTE_META_TRANSACTION_APPROVAL,
approvalSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
},
jobId,
);
const mockTransaction = createMetaTransactionV2SubmissionEntity(
{
from: '0xworkeraddress',
metaTransactionV2JobId: jobId,
maxFeePerGas: new BigNumber(100000),
maxPriorityFeePerGas: new BigNumber(100),
nonce: 0,
to: '0xexchangeproxyaddress',
transactionHash: '0xsignedtransactionhash',
type: RfqmTransactionSubmissionType.Approval,
status: RfqmTransactionSubmissionStatus.Submitted,
},
transactionSubmissionId,
);
const mockTransactionReceipt: providers.TransactionReceipt = {
blockHash: '0xblockhash',
blockNumber: 1,
byzantium: true,
confirmations: 3,
contractAddress: '0xexchangeproxyaddress',
cumulativeGasUsed: EthersBigNumber.from(1000),
effectiveGasPrice: EthersBigNumber.from(1000),
from: '0xworkeraddress',
gasUsed: EthersBigNumber.from(10000),
logs: [],
logsBloom: '',
status: 0,
to: '0xexchangeproxyaddress',
transactionHash: '0xsignedtransactionhash',
transactionIndex: 0,
type: 2,
};
const mockMinedBlock: providers.Block = {
_difficulty: EthersBigNumber.from(2),
difficulty: 2,
extraData: '',
gasLimit: EthersBigNumber.from(1000),
gasUsed: EthersBigNumber.from(1000),
hash: '0xblockhash',
miner: '0xminer',
nonce: '0x000',
number: 21,
parentHash: '0xparentblockhash',
timestamp: 12345,
transactions: ['0xsignedtransactionhash'],
};
const mockNonce = 0;
const gasStationAttendantMock = mock(GasStationAttendantEthereum);
when(gasStationAttendantMock.getExpectedTransactionGasRateAsync()).thenResolve(
new BigNumber(10).shiftedBy(GWEI_DECIMALS),
);
const mockDbUtils = mock(RfqmDbUtils);
when(
mockDbUtils.findMetaTransactionV2SubmissionsByJobIdAsync(
jobId,
deepEqual([RfqmTransactionSubmissionType.Approval]),
),
).thenResolve([mockTransaction]);
const updateRfqmJobCalledArgs: MetaTransactionV2JobEntity[] = [];
when(mockDbUtils.updateRfqmJobAsync(anything())).thenCall(async (jobArg) => {
updateRfqmJobCalledArgs.push(_.cloneDeep(jobArg));
});
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.isValidOrderSignerAsync(anything(), anything())).thenResolve(true);
when(mockBlockchainUtils.getMinOfBalancesAndAllowancesAsync(anything())).thenResolve([
new BigNumber(1000000000),
]);
when(mockBlockchainUtils.getTokenBalancesAsync(anything())).thenResolve([new BigNumber(1000000000)]);
when(mockBlockchainUtils.generateApprovalCalldataAsync(inputToken, anything(), anything())).thenResolve(
'0xcalldata',
);
when(mockBlockchainUtils.getNonceAsync('0xworkeraddress')).thenResolve(mockNonce);
when(mockBlockchainUtils.getReceiptsAsync(deepEqual(['0xsignedtransactionhash']))).thenResolve([
mockTransactionReceipt,
]);
when(mockBlockchainUtils.getBlockAsync('0xblockhash')).thenResolve(mockMinedBlock);
when(mockBlockchainUtils.getCurrentBlockAsync()).thenResolve(4);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
rfqBlockchainUtils: instance(mockBlockchainUtils),
});
await rfqmService.processApprovalAndTradeAsync(job, '0xworkeraddress');
expect(updateRfqmJobCalledArgs[0].status).to.equal(RfqmJobStatus.PendingProcessing);
expect(updateRfqmJobCalledArgs[updateRfqmJobCalledArgs.length - 1].status).to.equal(
RfqmJobStatus.FailedRevertedConfirmed,
);
expect(job.status).to.equal(RfqmJobStatus.FailedRevertedConfirmed);
});
it('should proceed to trade transaction if the status of approval transaction is `SucceededConfirmed` for a meta-transaction v2 job', async () => {
const nowS = Math.round(new Date().getTime() / ONE_SECOND_MS);
const jobId = 'jobId';
const transactionSubmissionId1 = 'submissionId1';
const transactionSubmissionId2 = 'submissionId2';
const inputToken = '0xinputToken';
const job = createMetaTransactionV2JobEntity(
{
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(nowS + 600),
inputToken,
inputTokenAmount: new BigNumber(10),
integratorId: '0xintegrator',
calledFunction: 'transformERC20',
metaTransaction: MOCK_META_TRANSACTION_V2,
metaTransactionHash: MOCK_META_TRANSACTION_V2.getHash(),
minOutputTokenAmount: new BigNumber(10),
outputToken: '0xoutputToken',
takerAddress: '0xtakerAddress',
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
tokens: ['0xinputToken', '0xoutputToken'],
status: RfqmJobStatus.PendingEnqueued,
workerAddress: '0xworkeraddress',
approval: MOCK_EXECUTE_META_TRANSACTION_APPROVAL,
approvalSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
},
jobId,
);
const mockApprovalTransaction = createMetaTransactionV2SubmissionEntity(
{
from: '0xworkeraddress',
metaTransactionV2JobId: jobId,
maxFeePerGas: new BigNumber(100000),
maxPriorityFeePerGas: new BigNumber(100),
nonce: 0,
to: '0xexchangeproxyaddress',
transactionHash: '0xsignedtransactionhash1',
type: RfqmTransactionSubmissionType.Approval,
status: RfqmTransactionSubmissionStatus.Submitted,
},
transactionSubmissionId1,
);
const mockTradeTransaction = createMetaTransactionV2SubmissionEntity(
{
from: '0xworkeraddress',
metaTransactionV2JobId: jobId,
maxFeePerGas: new BigNumber(100000),
maxPriorityFeePerGas: new BigNumber(100),
nonce: 0,
to: '0xexchangeproxyaddress',
transactionHash: '0xsignedtransactionhash2',
type: RfqmTransactionSubmissionType.Trade,
status: RfqmTransactionSubmissionStatus.Submitted,
},
transactionSubmissionId2,
);
const mockApprovalTransactionReceipt: providers.TransactionReceipt = {
blockHash: '0xblockhash',
blockNumber: 1,
byzantium: true,
confirmations: 3,
contractAddress: '0xexchangeproxyaddress',
cumulativeGasUsed: EthersBigNumber.from(1000),
effectiveGasPrice: EthersBigNumber.from(1000),
from: '0xworkeraddress',
gasUsed: EthersBigNumber.from(10000),
logs: [],
logsBloom: '',
status: 1,
to: '0xexchangeproxyaddress',
transactionHash: '0xsignedtransactionhash1',
transactionIndex: 0,
type: 2,
};
const mockTradeTransactionReceipt: providers.TransactionReceipt = {
blockHash: '0xblockhash',
blockNumber: 1,
byzantium: true,
confirmations: 3,
contractAddress: '0xexchangeproxyaddress',
cumulativeGasUsed: EthersBigNumber.from(1000),
effectiveGasPrice: EthersBigNumber.from(1000),
from: '0xworkeraddress',
gasUsed: EthersBigNumber.from(10000),
logs: [],
logsBloom: '',
status: 1,
to: '0xexchangeproxyaddress',
transactionHash: '0xsignedtransactionhash2',
transactionIndex: 0,
type: 2,
};
const mockMinedBlock: providers.Block = {
_difficulty: EthersBigNumber.from(2),
difficulty: 2,
extraData: '',
gasLimit: EthersBigNumber.from(1000),
gasUsed: EthersBigNumber.from(1000),
hash: '0xblockhash',
miner: '0xminer',
nonce: '0x000',
number: 21,
parentHash: '0xparentblockhash',
timestamp: 12345,
transactions: ['0xsignedtransactionhash1', '0xsignedtransactionhash2'],
};
const mockNonce = 0;
const gasStationAttendantMock = mock(GasStationAttendantEthereum);
when(gasStationAttendantMock.getExpectedTransactionGasRateAsync()).thenResolve(
new BigNumber(10).shiftedBy(GWEI_DECIMALS),
);
const mockDbUtils = mock(RfqmDbUtils);
when(
mockDbUtils.findMetaTransactionV2SubmissionsByJobIdAsync(
jobId,
deepEqual([RfqmTransactionSubmissionType.Approval]),
),
).thenResolve([mockApprovalTransaction]);
when(
mockDbUtils.findMetaTransactionV2SubmissionsByJobIdAsync(
jobId,
deepEqual([RfqmTransactionSubmissionType.Trade]),
),
).thenResolve([mockTradeTransaction]);
when(mockDbUtils.findMetaTransactionV2SubmissionsByJobIdAsync(jobId)).thenResolve([mockTradeTransaction]);
const updateRfqmJobCalledArgs: MetaTransactionJobEntity[] = [];
when(mockDbUtils.updateRfqmJobAsync(anything())).thenCall(async (jobArg) => {
updateRfqmJobCalledArgs.push(_.cloneDeep(jobArg));
});
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.isValidOrderSignerAsync(anything(), anything())).thenResolve(true);
when(mockBlockchainUtils.getMinOfBalancesAndAllowancesAsync(anything())).thenResolve([
new BigNumber(1000000000),
]);
when(mockBlockchainUtils.getTokenBalancesAsync(anything())).thenResolve([new BigNumber(1000000000)]);
when(mockBlockchainUtils.estimateGasForAsync(anything())).thenResolve(0);
when(
mockBlockchainUtils.generateMetaTransactionCallData(anything(), 'v2', anything(), anything()),
).thenReturn('0xcalldata');
when(mockBlockchainUtils.generateApprovalCalldataAsync(anything(), anything(), anything())).thenResolve(
'0xcalldata',
);
when(mockBlockchainUtils.getNonceAsync('0xworkeraddress')).thenResolve(mockNonce);
when(mockBlockchainUtils.getReceiptsAsync(deepEqual(['0xsignedtransactionhash1']))).thenResolve([
mockApprovalTransactionReceipt,
]);
when(mockBlockchainUtils.getReceiptsAsync(deepEqual(['0xsignedtransactionhash2']))).thenResolve([
mockTradeTransactionReceipt,
]);
when(mockBlockchainUtils.getBlockAsync('0xblockhash')).thenResolve(mockMinedBlock);
when(mockBlockchainUtils.getCurrentBlockAsync()).thenResolve(4);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
rfqBlockchainUtils: instance(mockBlockchainUtils),
});
await rfqmService.processApprovalAndTradeAsync(job, '0xworkeraddress');
expect(updateRfqmJobCalledArgs[0].status).to.equal(RfqmJobStatus.PendingProcessing);
expect(updateRfqmJobCalledArgs[1].status).to.equal(RfqmJobStatus.PendingSubmitted);
expect(updateRfqmJobCalledArgs[updateRfqmJobCalledArgs.length - 1].status).to.equal(
RfqmJobStatus.SucceededConfirmed,
);
expect(job.status).to.equal(RfqmJobStatus.SucceededConfirmed);
});
});
describe('processTradeAsync', () => {
it('should process a rfqm v2 job trade successfully', async () => {
const nowS = Math.round(new Date().getTime() / ONE_SECOND_MS);
const job = new RfqmV2JobEntity({
affiliateAddress: '',
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(nowS + 10),
fee: {
amount: '0',
token: '',
type: 'fixed',
},
integratorId: '',
lastLookResult: null,
makerUri: 'http://foo.bar',
makerSignature: null,
order: {
order: {
chainId: '1',
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
new BigNumber(nowS + 10),
new BigNumber(1),
new BigNumber(1),
).toString(),
maker,
makerAmount: '1000000',
makerToken: '0xmakertoken',
taker: '0xtaker',
takerAmount: '10000000',
takerToken: '0xtakertoken',
txOrigin: '',
verifyingContract: '',
},
type: RfqmOrderTypes.Otc,
},
orderHash,
status: RfqmJobStatus.PendingEnqueued,
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
updatedAt: new Date(),
workerAddress: '0xworkeraddress',
workflow: 'rfqm',
});
const mockTransactionRequest: providers.TransactionRequest = {};
const mockTransaction = new RfqmV2TransactionSubmissionEntity({
from: '0xworkeraddress',
maxFeePerGas: new BigNumber(100000),
maxPriorityFeePerGas: new BigNumber(100),
nonce: 0,
orderHash: '0x01234567',
to: '0xexchangeproxyaddress',
transactionHash: '0xsignedtransactionhash',
type: RfqmTransactionSubmissionType.Trade,
});
const mockTransactionReceipt: providers.TransactionReceipt = {
blockHash: '0xblockhash',
blockNumber: 1,
byzantium: true,
confirmations: 3,
contractAddress: '0xexchangeproxyaddress',
cumulativeGasUsed: EthersBigNumber.from(1000),
effectiveGasPrice: EthersBigNumber.from(1000),
from: '0xworkeraddress',
gasUsed: EthersBigNumber.from(10000),
logs: [],
logsBloom: '',
status: 1,
to: '0xexchangeproxyaddress',
transactionHash: '0xsignedtransactionhash',
transactionIndex: 0,
type: 2,
};
const mockMinedBlock: providers.Block = {
_difficulty: EthersBigNumber.from(2),
difficulty: 2,
extraData: '',
gasLimit: EthersBigNumber.from(1000),
gasUsed: EthersBigNumber.from(1000),
hash: '0xblockhash',
miner: '0xminer',
nonce: '0x000',
number: 21,
parentHash: '0xparentblockhash',
timestamp: 12345,
transactions: ['0xpresubmittransactionhash'],
};
const mockNonce = 0;
const gasStationAttendantMock = mock(GasStationAttendantEthereum);
when(gasStationAttendantMock.getExpectedTransactionGasRateAsync()).thenResolve(
new BigNumber(10).shiftedBy(GWEI_DECIMALS),
);
const mockDbUtils = mock(RfqmDbUtils);
when(mockDbUtils.findV2TransactionSubmissionsByOrderHashAsync(anything())).thenResolve([]);
const updateRfqmJobCalledArgs: RfqmJobEntity[] = [];
when(mockDbUtils.updateRfqmJobAsync(anything())).thenCall(async (jobArg) => {
updateRfqmJobCalledArgs.push(_.cloneDeep(jobArg));
});
when(
mockDbUtils.findV2TransactionSubmissionsByOrderHashAsync(
anything(),
deepEqual([RfqmTransactionSubmissionType.Trade]),
),
).thenResolve([]);
when(mockDbUtils.findV2TransactionSubmissionByTransactionHashAsync('0xsignedtransactionhash')).thenResolve(
_.cloneDeep(mockTransaction),
);
const mockQuoteServerClient = mock(QuoteServerClient);
when(mockQuoteServerClient.signV2Async(anything(), anything(), anything())).thenResolve(validEIP712Sig);
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.getExchangeProxyAddress()).thenReturn('0xexchangeproxyaddress');
when(mockBlockchainUtils.getMinOfBalancesAndAllowancesAsync(anything())).thenResolve([
new BigNumber(1000000000),
]);
when(
mockBlockchainUtils.generateTakerSignedOtcOrderCallData(
anything(),
anything(),
anything(),
anything(),
anything(),
),
).thenReturn('0xcalldata');
when(mockBlockchainUtils.estimateGasForAsync(anything())).thenResolve(0);
when(mockBlockchainUtils.getNonceAsync('0xworkeraddress')).thenResolve(mockNonce);
when(mockBlockchainUtils.estimateGasForAsync(anything())).thenResolve(100);
when(
mockBlockchainUtils.transformTxDataToTransactionRequest(anything(), anything(), anything()),
).thenReturn(mockTransactionRequest);
when(mockBlockchainUtils.submitSignedTransactionAsync(anything())).thenResolve('0xsignedtransactionhash');
when(mockBlockchainUtils.signTransactionAsync(anything())).thenResolve({
signedTransaction: 'signedTransaction',
transactionHash: '0xsignedtransactionhash',
});
when(mockBlockchainUtils.getReceiptsAsync(deepEqual(['0xsignedtransactionhash']))).thenResolve([
mockTransactionReceipt,
]);
when(mockBlockchainUtils.getBlockAsync('0xblockhash')).thenResolve(mockMinedBlock);
when(mockBlockchainUtils.getCurrentBlockAsync()).thenResolve(4);
const mockRfqMakerBalanceCacheService = mock(RfqMakerBalanceCacheService);
when(mockRfqMakerBalanceCacheService.getERC20OwnerBalancesAsync(anything(), anything())).thenResolve([
new BigNumber(1000000000),
]);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
quoteServerClient: instance(mockQuoteServerClient),
rfqBlockchainUtils: instance(mockBlockchainUtils),
rfqMakerBalanceCacheService: instance(mockRfqMakerBalanceCacheService),
});
await rfqmService.processTradeAsync(job, '0xworkeraddress');
expect(updateRfqmJobCalledArgs[0].status).to.equal(RfqmJobStatus.PendingProcessing);
expect(updateRfqmJobCalledArgs[1].status).to.equal(RfqmJobStatus.PendingLastLookAccepted);
expect(updateRfqmJobCalledArgs[2].status).to.equal(RfqmJobStatus.PendingSubmitted);
expect(updateRfqmJobCalledArgs[updateRfqmJobCalledArgs.length - 1].status).to.equal(
RfqmJobStatus.SucceededConfirmed,
);
expect(job.status).to.equal(RfqmJobStatus.SucceededConfirmed);
});
it('should process a meta-transaction job trade successfully', async () => {
const nowS = Math.round(new Date().getTime() / ONE_SECOND_MS);
const jobId = 'jobId';
const transactionSubmissionId = 'submissionId';
const job = createMetaTransactionJobEntity(
{
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(nowS + 600),
fee: {
amount: new BigNumber(0),
token: '',
type: 'fixed',
},
inputToken: '0xinputToken',
inputTokenAmount: new BigNumber(10),
integratorId: '0xintegrator',
metaTransaction: MOCK_META_TRANSACTION,
metaTransactionHash: MOCK_META_TRANSACTION.getHash(),
minOutputTokenAmount: new BigNumber(10),
outputToken: '0xoutputToken',
takerAddress: '0xtakerAddress',
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
status: RfqmJobStatus.PendingEnqueued,
workerAddress: '0xworkeraddress',
},
jobId,
);
const mockTransactionRequest: providers.TransactionRequest = {};
const mockTransaction = createMetaTransactionSubmissionEntity(
{
from: '0xworkeraddress',
metaTransactionJobId: jobId,
maxFeePerGas: new BigNumber(100000),
maxPriorityFeePerGas: new BigNumber(100),
nonce: 0,
to: '0xexchangeproxyaddress',
transactionHash: '0xsignedtransactionhash',
type: RfqmTransactionSubmissionType.Trade,
},
transactionSubmissionId,
);
const mockTransactionReceipt: providers.TransactionReceipt = {
blockHash: '0xblockhash',
blockNumber: 1,
byzantium: true,
confirmations: 3,
contractAddress: '0xexchangeproxyaddress',
cumulativeGasUsed: EthersBigNumber.from(1000),
effectiveGasPrice: EthersBigNumber.from(1000),
from: '0xworkeraddress',
gasUsed: EthersBigNumber.from(10000),
logs: [],
logsBloom: '',
status: 1,
to: '0xexchangeproxyaddress',
transactionHash: '0xsignedtransactionhash',
transactionIndex: 0,
type: 2,
};
const mockMinedBlock: providers.Block = {
_difficulty: EthersBigNumber.from(2),
difficulty: 2,
extraData: '',
gasLimit: EthersBigNumber.from(1000),
gasUsed: EthersBigNumber.from(1000),
hash: '0xblockhash',
miner: '0xminer',
nonce: '0x000',
number: 21,
parentHash: '0xparentblockhash',
timestamp: 12345,
transactions: ['0xpresubmittransactionhash'],
};
const mockNonce = 0;
const gasStationAttendantMock = mock(GasStationAttendantEthereum);
when(gasStationAttendantMock.getExpectedTransactionGasRateAsync()).thenResolve(
new BigNumber(10).shiftedBy(GWEI_DECIMALS),
);
const mockDbUtils = mock(RfqmDbUtils);
when(mockDbUtils.findMetaTransactionSubmissionsByJobIdAsync(jobId)).thenResolve([]);
const updateRfqmJobCalledArgs: MetaTransactionJobEntity[] = [];
when(mockDbUtils.updateRfqmJobAsync(anything())).thenCall(async (jobArg) => {
updateRfqmJobCalledArgs.push(_.cloneDeep(jobArg));
});
when(
mockDbUtils.findMetaTransactionSubmissionsByJobIdAsync(
jobId,
deepEqual([RfqmTransactionSubmissionType.Trade]),
),
).thenResolve([]);
when(
mockDbUtils.findMetaTransactionSubmissionsByTransactionHashAsync(
'0xsignedtransactionhash',
RfqmTransactionSubmissionType.Trade,
),
).thenResolve([_.cloneDeep(mockTransaction)]);
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.getExchangeProxyAddress()).thenReturn('0xexchangeproxyaddress');
when(
mockBlockchainUtils.generateMetaTransactionCallData(anything(), 'v1', anything(), anything()),
).thenReturn('0xcalldata');
when(mockBlockchainUtils.estimateGasForAsync(anything())).thenResolve(0);
when(mockBlockchainUtils.getNonceAsync('0xworkeraddress')).thenResolve(mockNonce);
when(
mockBlockchainUtils.transformTxDataToTransactionRequest(anything(), anything(), anything()),
).thenReturn(mockTransactionRequest);
when(mockBlockchainUtils.submitSignedTransactionAsync(anything())).thenResolve('0xsignedtransactionhash');
when(mockBlockchainUtils.signTransactionAsync(anything())).thenResolve({
signedTransaction: 'signedTransaction',
transactionHash: '0xsignedtransactionhash',
});
when(mockBlockchainUtils.getReceiptsAsync(deepEqual(['0xsignedtransactionhash']))).thenResolve([
mockTransactionReceipt,
]);
when(mockBlockchainUtils.getBlockAsync('0xblockhash')).thenResolve(mockMinedBlock);
when(mockBlockchainUtils.getCurrentBlockAsync()).thenResolve(4);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
rfqBlockchainUtils: instance(mockBlockchainUtils),
});
await rfqmService.processTradeAsync(job, '0xworkeraddress');
expect(updateRfqmJobCalledArgs[0].status).to.equal(RfqmJobStatus.PendingProcessing);
expect(updateRfqmJobCalledArgs[1].status).to.equal(RfqmJobStatus.PendingSubmitted);
expect(updateRfqmJobCalledArgs[updateRfqmJobCalledArgs.length - 1].status).to.equal(
RfqmJobStatus.SucceededConfirmed,
);
expect(job.status).to.equal(RfqmJobStatus.SucceededConfirmed);
});
it('should process a meta-transaction v2 job trade successfully', async () => {
const nowS = Math.round(new Date().getTime() / ONE_SECOND_MS);
const jobId = 'jobId';
const transactionSubmissionId = 'submissionId';
const job = createMetaTransactionV2JobEntity(
{
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(nowS + 600),
inputToken: '0xinputToken',
inputTokenAmount: new BigNumber(10),
integratorId: '0xintegrator',
calledFunction: 'transformERC20',
metaTransaction: MOCK_META_TRANSACTION_V2,
metaTransactionHash: MOCK_META_TRANSACTION_V2.getHash(),
minOutputTokenAmount: new BigNumber(10),
outputToken: '0xoutputToken',
takerAddress: '0xtakerAddress',
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
tokens: ['0xinputToken', '0xoutputToken'],
status: RfqmJobStatus.PendingEnqueued,
workerAddress: '0xworkeraddress',
},
jobId,
);
const mockTransactionRequest: providers.TransactionRequest = {};
const mockTransaction = createMetaTransactionV2SubmissionEntity(
{
from: '0xworkeraddress',
metaTransactionV2JobId: jobId,
maxFeePerGas: new BigNumber(100000),
maxPriorityFeePerGas: new BigNumber(100),
nonce: 0,
to: '0xexchangeproxyaddress',
transactionHash: '0xsignedtransactionhash',
type: RfqmTransactionSubmissionType.Trade,
status: RfqmTransactionSubmissionStatus.Submitted,
},
transactionSubmissionId,
);
const mockTransactionReceipt: providers.TransactionReceipt = {
blockHash: '0xblockhash',
blockNumber: 1,
byzantium: true,
confirmations: 3,
contractAddress: '0xexchangeproxyaddress',
cumulativeGasUsed: EthersBigNumber.from(1000),
effectiveGasPrice: EthersBigNumber.from(1000),
from: '0xworkeraddress',
gasUsed: EthersBigNumber.from(10000),
logs: [],
logsBloom: '',
status: 1,
to: '0xexchangeproxyaddress',
transactionHash: '0xsignedtransactionhash',
transactionIndex: 0,
type: 2,
};
const mockMinedBlock: providers.Block = {
_difficulty: EthersBigNumber.from(2),
difficulty: 2,
extraData: '',
gasLimit: EthersBigNumber.from(1000),
gasUsed: EthersBigNumber.from(1000),
hash: '0xblockhash',
miner: '0xminer',
nonce: '0x000',
number: 21,
parentHash: '0xparentblockhash',
timestamp: 12345,
transactions: ['0xpresubmittransactionhash'],
};
const mockNonce = 0;
const gasStationAttendantMock = mock(GasStationAttendantEthereum);
when(gasStationAttendantMock.getExpectedTransactionGasRateAsync()).thenResolve(
new BigNumber(10).shiftedBy(GWEI_DECIMALS),
);
const mockDbUtils = mock(RfqmDbUtils);
when(mockDbUtils.findMetaTransactionV2SubmissionsByJobIdAsync(jobId)).thenResolve([]);
const updateRfqmJobCalledArgs: MetaTransactionJobEntity[] = [];
when(mockDbUtils.updateRfqmJobAsync(anything())).thenCall(async (jobArg) => {
updateRfqmJobCalledArgs.push(_.cloneDeep(jobArg));
});
when(
mockDbUtils.findMetaTransactionV2SubmissionsByJobIdAsync(
jobId,
deepEqual([RfqmTransactionSubmissionType.Trade]),
),
).thenResolve([]);
when(
mockDbUtils.findMetaTransactionV2SubmissionsByTransactionHashAsync(
'0xsignedtransactionhash',
RfqmTransactionSubmissionType.Trade,
),
).thenResolve([_.cloneDeep(mockTransaction)]);
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.getExchangeProxyAddress()).thenReturn('0xexchangeproxyaddress');
when(
mockBlockchainUtils.generateMetaTransactionCallData(anything(), 'v2', anything(), anything()),
).thenReturn('0xcalldata');
when(mockBlockchainUtils.estimateGasForAsync(anything())).thenResolve(0);
when(mockBlockchainUtils.getNonceAsync('0xworkeraddress')).thenResolve(mockNonce);
when(
mockBlockchainUtils.transformTxDataToTransactionRequest(anything(), anything(), anything()),
).thenReturn(mockTransactionRequest);
when(mockBlockchainUtils.submitSignedTransactionAsync(anything())).thenResolve('0xsignedtransactionhash');
when(mockBlockchainUtils.signTransactionAsync(anything())).thenResolve({
signedTransaction: 'signedTransaction',
transactionHash: '0xsignedtransactionhash',
});
when(mockBlockchainUtils.getReceiptsAsync(deepEqual(['0xsignedtransactionhash']))).thenResolve([
mockTransactionReceipt,
]);
when(mockBlockchainUtils.getBlockAsync('0xblockhash')).thenResolve(mockMinedBlock);
when(mockBlockchainUtils.getCurrentBlockAsync()).thenResolve(4);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
rfqBlockchainUtils: instance(mockBlockchainUtils),
});
await rfqmService.processTradeAsync(job, '0xworkeraddress');
expect(updateRfqmJobCalledArgs[0].status).to.equal(RfqmJobStatus.PendingProcessing);
expect(updateRfqmJobCalledArgs[1].status).to.equal(RfqmJobStatus.PendingSubmitted);
expect(updateRfqmJobCalledArgs[updateRfqmJobCalledArgs.length - 1].status).to.equal(
RfqmJobStatus.SucceededConfirmed,
);
expect(job.status).to.equal(RfqmJobStatus.SucceededConfirmed);
});
});
describe('validate job methods', () => {
it('should return null for valid, unexpired RFQm v2 jobs', () => {
const fakeInFiveMinutesS = fakeClockMs / ONE_SECOND_MS + 360;
const job = new RfqmV2JobEntity({
affiliateAddress: '',
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeInFiveMinutesS),
fee: {
amount: '0',
token: '',
type: 'fixed',
},
integratorId: '',
lastLookResult: null,
makerUri: 'http://foo.bar',
order: {
order: {
chainId: '1',
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
/* expiry */ new BigNumber(fakeInFiveMinutesS),
/* nonceBucket */ new BigNumber(21),
/* nonce */ new BigNumber(0),
).toString(),
maker: '',
makerAmount: '',
makerToken: '',
taker: '',
takerAmount: '',
takerToken: '',
txOrigin: '',
verifyingContract: '',
},
type: RfqmOrderTypes.Otc,
},
orderHash: '',
status: RfqmJobStatus.PendingEnqueued,
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
updatedAt: new Date(),
workerAddress: '',
workflow: 'rfqm',
});
const result = WorkerService.validateRfqmV2Job(job, new Date(fakeClockMs));
expect(result).to.equal(null);
});
it('should return a No Taker Signature status for RFQm v2 jobs with no taker signature', () => {
const fakeInFiveMinutesS = fakeClockMs / ONE_SECOND_MS + 360;
const job = new RfqmV2JobEntity({
affiliateAddress: '',
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeInFiveMinutesS),
fee: {
amount: '0',
token: '',
type: 'fixed',
},
integratorId: '',
lastLookResult: null,
makerUri: 'http://foo.bar',
order: {
order: {
chainId: '1',
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
/* expiry */ new BigNumber(fakeInFiveMinutesS),
/* nonceBucket */ new BigNumber(21),
/* nonce */ new BigNumber(0),
).toString(),
maker: '',
makerAmount: '',
makerToken: '',
taker: '',
takerAmount: '',
takerToken: '',
txOrigin: '',
verifyingContract: '',
},
type: RfqmOrderTypes.Otc,
},
orderHash: '',
status: RfqmJobStatus.PendingEnqueued,
takerSignature: null,
updatedAt: new Date(),
workerAddress: '',
workflow: 'rfqm',
});
const result = WorkerService.validateRfqmV2Job(job, new Date(fakeClockMs));
expect(result).to.equal(RfqmJobStatus.FailedValidationNoTakerSignature);
});
it('should return null for valid, unexpired meta-transaction v1 and v2 jobs', () => {
const fakeInFiveMinutesS = fakeClockMs / ONE_SECOND_MS + 360;
const v1Job = createMetaTransactionJobEntity(
{
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeInFiveMinutesS),
fee: {
amount: new BigNumber(0),
token: '',
type: 'fixed',
},
inputToken: '0xinputToken',
inputTokenAmount: new BigNumber(10),
integratorId: '0xintegrator',
metaTransaction: MOCK_META_TRANSACTION,
metaTransactionHash: MOCK_META_TRANSACTION.getHash(),
minOutputTokenAmount: new BigNumber(10),
outputToken: '0xoutputToken',
takerAddress: '0xtakerAddress',
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
status: RfqmJobStatus.PendingEnqueued,
workerAddress: '0xworkeraddress',
},
'jobId',
);
const v2Job = createMetaTransactionV2JobEntity(
{
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
inputToken: '0xinputToken',
inputTokenAmount: new BigNumber(10),
integratorId: '0xintegrator',
calledFunction: 'transformERC20',
metaTransaction: MOCK_META_TRANSACTION_V2,
metaTransactionHash: MOCK_META_TRANSACTION_V2.getHash(),
minOutputTokenAmount: new BigNumber(10),
outputToken: '0xoutputToken',
takerAddress: '0xtakerAddress',
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
tokens: ['0xinputToken', '0xoutputToken'],
status: RfqmJobStatus.PendingEnqueued,
workerAddress: '0xworkeraddress',
},
'jobId',
);
expect(WorkerService.validateMetaTransactionJob(v1Job, new Date(fakeClockMs))).to.equal(null);
expect(WorkerService.validateMetaTransactionJob(v2Job, new Date(fakeClockMs))).to.equal(null);
});
it('should return a failed expired status for meta-transaction v1 and v2 job that expire', () => {
const v1Job = createMetaTransactionJobEntity(
{
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeClockMs / ONE_SECOND_MS),
fee: {
amount: new BigNumber(0),
token: '',
type: 'fixed',
},
inputToken: '0xinputToken',
inputTokenAmount: new BigNumber(10),
integratorId: '0xintegrator',
metaTransaction: MOCK_META_TRANSACTION,
metaTransactionHash: MOCK_META_TRANSACTION.getHash(),
minOutputTokenAmount: new BigNumber(10),
outputToken: '0xoutputToken',
takerAddress: '0xtakerAddress',
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
status: RfqmJobStatus.PendingEnqueued,
workerAddress: '0xworkeraddress',
},
'jobId',
);
const v2Job = createMetaTransactionV2JobEntity(
{
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeClockMs / ONE_SECOND_MS),
inputToken: '0xinputToken',
inputTokenAmount: new BigNumber(10),
integratorId: '0xintegrator',
calledFunction: 'transformERC20',
metaTransaction: MOCK_META_TRANSACTION_V2,
metaTransactionHash: MOCK_META_TRANSACTION_V2.getHash(),
minOutputTokenAmount: new BigNumber(10),
outputToken: '0xoutputToken',
takerAddress: '0xtakerAddress',
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
tokens: ['0xinputToken', '0xoutputToken'],
status: RfqmJobStatus.PendingEnqueued,
workerAddress: '0xworkeraddress',
},
'jobId',
);
expect(WorkerService.validateMetaTransactionJob(v1Job, new Date(fakeClockMs + 1000000))).to.equal(
RfqmJobStatus.FailedExpired,
);
expect(WorkerService.validateMetaTransactionJob(v2Job, new Date(fakeClockMs + 1000000))).to.equal(
RfqmJobStatus.FailedExpired,
);
});
});
describe('shouldResubmitTransaction', () => {
it('should return false if new gas price < 10% greater than previous', async () => {
const gasFees = { maxFeePerGas: new BigNumber(100), maxPriorityFeePerGas: new BigNumber(10) };
const newGasPrice = new BigNumber(105);
expect(WorkerService.shouldResubmitTransaction(gasFees, newGasPrice)).to.equal(false);
});
it('should return true if new gas price is 10% greater than previous', async () => {
const gasFees = { maxFeePerGas: new BigNumber(100), maxPriorityFeePerGas: new BigNumber(10) };
const newGasPrice = new BigNumber(110);
expect(WorkerService.shouldResubmitTransaction(gasFees, newGasPrice)).to.equal(true);
});
it('should return true if new gas price > 10% greater than previous', async () => {
const gasFees = { maxFeePerGas: new BigNumber(100), maxPriorityFeePerGas: new BigNumber(10) };
const newGasPrice = new BigNumber(120);
expect(WorkerService.shouldResubmitTransaction(gasFees, newGasPrice)).to.equal(true);
});
});
describe('checkJobPreprocessingAsync', () => {
it('should update job staus and throw error if job validation failed for a rfqm v2 job', async () => {
const job = new RfqmV2JobEntity({
affiliateAddress: '',
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeOneMinuteAgoS),
fee: {
amount: '0',
token: '',
type: 'fixed',
},
integratorId: '',
lastLookResult: null,
makerUri: 'http://foo.bar',
makerSignature: null,
order: {
order: {
chainId: '1',
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
new BigNumber(fakeOneMinuteAgoS.toString()),
new BigNumber(1),
new BigNumber(1),
).toString(),
maker,
makerAmount: '1000000',
makerToken: '0xmakertoken',
taker: '0xtaker',
takerAmount: '10000000',
takerToken: '0xtakertoken',
txOrigin: '',
verifyingContract: '',
},
type: RfqmOrderTypes.Otc,
},
orderHash,
status: RfqmJobStatus.PendingEnqueued,
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
updatedAt: new Date(),
workerAddress: '',
workflow: 'rfqm',
});
const mockDbUtils = mock(RfqmDbUtils);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
});
try {
await rfqmService.checkJobPreprocessingAsync(job, new Date(fakeClockMs));
expect.fail();
} catch (e) {
expect(e.message).to.contain('Job failed validation');
expect(job.status).to.deep.equal(RfqmJobStatus.FailedExpired);
}
});
it('should throw error if there is no taker signature for a rfqm v2 job', async () => {
const job = new RfqmV2JobEntity({
affiliateAddress: '',
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
fee: {
amount: '0',
token: '',
type: 'fixed',
},
integratorId: '',
lastLookResult: null,
makerUri: 'http://foo.bar',
makerSignature: null,
order: {
order: {
chainId: '1',
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
new BigNumber(fakeFiveMinutesLater.toString()),
new BigNumber(1),
new BigNumber(1),
).toString(),
maker,
makerAmount: '1000000',
makerToken: '0xmakertoken',
taker: '0xtaker',
takerAmount: '10000000',
takerToken: '0xtakertoken',
txOrigin: '',
verifyingContract: '',
},
type: RfqmOrderTypes.Otc,
},
orderHash,
status: RfqmJobStatus.PendingEnqueued,
takerSignature: null,
updatedAt: new Date(),
workerAddress: '',
workflow: 'rfqm',
});
const mockDbUtils = mock(RfqmDbUtils);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
});
try {
await rfqmService.checkJobPreprocessingAsync(job, new Date(fakeClockMs));
expect.fail();
} catch (e) {
expect(e.message).to.contain('Job failed validation');
expect(job.status).to.deep.equal(RfqmJobStatus.FailedValidationNoTakerSignature);
}
});
it('should update job staus to `PendingProcessing` if job status is `PendingEnqueued` for a rfqm v2 job', async () => {
const job = new RfqmV2JobEntity({
affiliateAddress: '',
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
fee: {
amount: '0',
token: '',
type: 'fixed',
},
integratorId: '',
lastLookResult: null,
makerUri: 'http://foo.bar',
makerSignature: null,
order: {
order: {
chainId: '1',
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
new BigNumber(fakeFiveMinutesLater.toString()),
new BigNumber(1),
new BigNumber(1),
).toString(),
maker,
makerAmount: '1000000',
makerToken: '0xmakertoken',
taker: '0xtaker',
takerAmount: '10000000',
takerToken: '0xtakertoken',
txOrigin: '',
verifyingContract: '',
},
type: RfqmOrderTypes.Otc,
},
orderHash,
status: RfqmJobStatus.PendingEnqueued,
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
updatedAt: new Date(),
workerAddress: '',
workflow: 'rfqm',
});
const mockDbUtils = mock(RfqmDbUtils);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
});
await rfqmService.checkJobPreprocessingAsync(job, new Date(fakeClockMs));
expect(job.status).to.deep.equal(RfqmJobStatus.PendingProcessing);
});
it('should update job staus and throw error if job validation failed for a meta-transaction job', async () => {
const job = createMetaTransactionJobEntity(
{
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeOneMinuteAgoS),
fee: {
amount: new BigNumber(0),
token: '',
type: 'fixed',
},
inputToken: '0xinputToken',
inputTokenAmount: new BigNumber(10),
integratorId: '0xintegrator',
metaTransaction: MOCK_META_TRANSACTION,
metaTransactionHash: MOCK_META_TRANSACTION.getHash(),
minOutputTokenAmount: new BigNumber(10),
outputToken: '0xoutputToken',
takerAddress: '0xtakerAddress',
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
status: RfqmJobStatus.PendingEnqueued,
workerAddress: '0xworkeraddress',
},
'jobId',
);
const mockDbUtils = mock(RfqmDbUtils);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
});
try {
await rfqmService.checkJobPreprocessingAsync(job, new Date(fakeClockMs));
expect.fail();
} catch (e) {
expect(e.message).to.contain('Job failed validation');
expect(job.status).to.deep.equal(RfqmJobStatus.FailedExpired);
}
});
it('should update job staus to `PendingProcessing` if job status is `PendingEnqueued` for a meta-transaction job', async () => {
const job = createMetaTransactionJobEntity(
{
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
fee: {
amount: new BigNumber(0),
token: '',
type: 'fixed',
},
inputToken: '0xinputToken',
inputTokenAmount: new BigNumber(10),
integratorId: '0xintegrator',
metaTransaction: MOCK_META_TRANSACTION,
metaTransactionHash: MOCK_META_TRANSACTION.getHash(),
minOutputTokenAmount: new BigNumber(10),
outputToken: '0xoutputToken',
takerAddress: '0xtakerAddress',
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
status: RfqmJobStatus.PendingEnqueued,
workerAddress: '0xworkeraddress',
},
'jobId',
);
const mockDbUtils = mock(RfqmDbUtils);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
});
await rfqmService.checkJobPreprocessingAsync(job, new Date(fakeClockMs));
expect(job.status).to.deep.equal(RfqmJobStatus.PendingProcessing);
});
it('should update job staus and throw error if job validation failed for a meta-transaction v2 job', async () => {
const job = createMetaTransactionV2JobEntity(
{
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeOneMinuteAgoS),
inputToken: '0xinputToken',
inputTokenAmount: new BigNumber(10),
integratorId: '0xintegrator',
calledFunction: 'transformERC20',
metaTransaction: MOCK_META_TRANSACTION_V2,
metaTransactionHash: MOCK_META_TRANSACTION_V2.getHash(),
minOutputTokenAmount: new BigNumber(10),
outputToken: '0xoutputToken',
takerAddress: '0xtakerAddress',
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
tokens: ['0xinputToken', '0xoutputToken'],
status: RfqmJobStatus.PendingEnqueued,
workerAddress: '0xworkeraddress',
},
'jobId',
);
const mockDbUtils = mock(RfqmDbUtils);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
});
try {
await rfqmService.checkJobPreprocessingAsync(job, new Date(fakeClockMs));
expect.fail();
} catch (e) {
expect(e.message).to.contain('Job failed validation');
expect(job.status).to.deep.equal(RfqmJobStatus.FailedExpired);
}
});
it('should update job staus to `PendingProcessing` if job status is `PendingEnqueued` for a meta-transaction v2 job', async () => {
const job = createMetaTransactionV2JobEntity(
{
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
inputToken: '0xinputToken',
inputTokenAmount: new BigNumber(10),
integratorId: '0xintegrator',
calledFunction: 'transformERC20',
metaTransaction: MOCK_META_TRANSACTION_V2,
metaTransactionHash: MOCK_META_TRANSACTION_V2.getHash(),
minOutputTokenAmount: new BigNumber(10),
outputToken: '0xoutputToken',
takerAddress: '0xtakerAddress',
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
tokens: ['0xinputToken', '0xoutputToken'],
status: RfqmJobStatus.PendingEnqueued,
workerAddress: '0xworkeraddress',
},
'jobId',
);
const mockDbUtils = mock(RfqmDbUtils);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
});
await rfqmService.checkJobPreprocessingAsync(job, new Date(fakeClockMs));
expect(job.status).to.deep.equal(RfqmJobStatus.PendingProcessing);
});
});
describe('prepareApprovalAsync', () => {
it('should throw exception if there are submitted transactions but job maker signature is null for a rfqm v2 job', async () => {
const job = new RfqmV2JobEntity({
affiliateAddress: '',
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
fee: {
amount: '0',
token: '',
type: 'fixed',
},
integratorId: '',
lastLookResult: null,
makerUri: 'http://foo.bar',
makerSignature: null,
order: {
order: {
chainId: '1',
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
new BigNumber(fakeFiveMinutesLater.toString()),
new BigNumber(1),
new BigNumber(1),
).toString(),
maker: '0xmaker',
makerAmount: '1000000',
makerToken: '0xmakertoken',
taker: '0xtaker',
takerAmount: '10000000',
takerToken: '0xtakertoken',
txOrigin: '',
verifyingContract: '',
},
type: RfqmOrderTypes.Otc,
},
orderHash: '0x01234567',
status: RfqmJobStatus.PendingEnqueued,
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
updatedAt: new Date(),
workerAddress: '',
workflow: 'rfqm',
});
const mockTransaction = new RfqmV2TransactionSubmissionEntity({
createdAt: new Date(1233),
from: '0xworkeraddress',
maxFeePerGas: new BigNumber(100000),
maxPriorityFeePerGas: new BigNumber(100),
nonce: 0,
orderHash: '0x01234567',
status: RfqmTransactionSubmissionStatus.Submitted,
to: '0xexchangeproxyaddress',
transactionHash: '0xpresubmittransactionhash',
type: RfqmTransactionSubmissionType.Trade,
});
const mockDbUtils = mock(RfqmDbUtils);
when(
mockDbUtils.findV2TransactionSubmissionsByOrderHashAsync(
'0x01234567',
deepEqual([RfqmTransactionSubmissionType.Approval]),
),
).thenResolve([mockTransaction]);
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.generateApprovalCalldataAsync(anything(), anything(), anything())).thenResolve(
MOCK_EXECUTE_META_TRANSACTION_CALLDATA,
);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
rfqBlockchainUtils: instance(mockBlockchainUtils),
});
try {
await rfqmService.prepareApprovalAsync(
job,
'0xworkeraddress',
'0xtoken',
MOCK_EXECUTE_META_TRANSACTION_APPROVAL,
{
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
);
expect.fail();
} catch (e) {
expect(e.message).to.contain('Encountered a job with submissions but no maker signature');
}
});
it('should return generated calldata if there are submitted transactions for a rfqm v2 job', async () => {
const job = new RfqmV2JobEntity({
affiliateAddress: '',
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
fee: {
amount: '0',
token: '',
type: 'fixed',
},
integratorId: '',
lastLookResult: null,
makerUri: 'http://foo.bar',
makerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
order: {
order: {
chainId: '1',
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
new BigNumber(fakeFiveMinutesLater.toString()),
new BigNumber(1),
new BigNumber(1),
).toString(),
maker: '0xmaker',
makerAmount: '1000000',
makerToken: '0xmakertoken',
taker: '0xtaker',
takerAmount: '10000000',
takerToken: '0xtakertoken',
txOrigin: '',
verifyingContract: '',
},
type: RfqmOrderTypes.Otc,
},
orderHash: '0x01234567',
status: RfqmJobStatus.PendingEnqueued,
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
updatedAt: new Date(),
workerAddress: '',
workflow: 'rfqm',
});
const mockTransaction = new RfqmV2TransactionSubmissionEntity({
createdAt: new Date(1233),
from: '0xworkeraddress',
maxFeePerGas: new BigNumber(100000),
maxPriorityFeePerGas: new BigNumber(100),
nonce: 0,
orderHash: '0x01234567',
status: RfqmTransactionSubmissionStatus.Submitted,
to: '0xexchangeproxyaddress',
transactionHash: '0xpresubmittransactionhash',
type: RfqmTransactionSubmissionType.Trade,
});
const mockDbUtils = mock(RfqmDbUtils);
when(
mockDbUtils.findV2TransactionSubmissionsByOrderHashAsync(
'0x01234567',
deepEqual([RfqmTransactionSubmissionType.Approval]),
),
).thenResolve([mockTransaction]);
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.generateApprovalCalldataAsync(anything(), anything(), anything())).thenResolve(
MOCK_EXECUTE_META_TRANSACTION_CALLDATA,
);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
rfqBlockchainUtils: instance(mockBlockchainUtils),
});
const calldata = await rfqmService.prepareApprovalAsync(
job,
'0xworkeraddress',
'0xtoken',
MOCK_EXECUTE_META_TRANSACTION_APPROVAL,
{
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
);
expect(calldata).to.deep.equal(MOCK_EXECUTE_META_TRANSACTION_CALLDATA);
});
it('should throw exception if eth_call failed for a rfqm v2 job', async () => {
const job = new RfqmV2JobEntity({
affiliateAddress: '',
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
fee: {
amount: '0',
token: '',
type: 'fixed',
},
integratorId: '',
lastLookResult: null,
makerUri: 'http://foo.bar',
makerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
order: {
order: {
chainId: '1',
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
new BigNumber(fakeFiveMinutesLater.toString()),
new BigNumber(1),
new BigNumber(1),
).toString(),
maker: '0xmaker',
makerAmount: '1000000',
makerToken: '0xmakertoken',
taker: '0xtaker',
takerAmount: '10000000',
takerToken: '0xtakertoken',
txOrigin: '',
verifyingContract: '',
},
type: RfqmOrderTypes.Otc,
},
orderHash: '0x01234567',
status: RfqmJobStatus.PendingEnqueued,
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
updatedAt: new Date(),
workerAddress: '',
workflow: 'rfqm',
});
const mockDbUtils = mock(RfqmDbUtils);
when(
mockDbUtils.findV2TransactionSubmissionsByOrderHashAsync(
'0x01234567',
deepEqual([RfqmTransactionSubmissionType.Approval]),
),
).thenResolve([]);
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.generateApprovalCalldataAsync(anything(), anything(), anything())).thenResolve(
MOCK_EXECUTE_META_TRANSACTION_CALLDATA,
);
when(mockBlockchainUtils.estimateGasForAsync(anything())).thenThrow(new Error('error'));
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
rfqBlockchainUtils: instance(mockBlockchainUtils),
});
try {
await rfqmService.prepareApprovalAsync(
job,
'0xworkeraddress',
'0xtoken',
MOCK_EXECUTE_META_TRANSACTION_APPROVAL,
{
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
);
expect.fail();
} catch (e) {
expect(e.message).to.contain('Eth call approval validation failed');
expect(job.status).to.deep.equal(RfqmJobStatus.FailedEthCallFailed);
}
});
it('should return correct calldata if there is no submitted transaction for a rfqm v2 job', async () => {
const job = new RfqmV2JobEntity({
affiliateAddress: '',
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
fee: {
amount: '0',
token: '',
type: 'fixed',
},
integratorId: '',
lastLookResult: null,
makerUri: 'http://foo.bar',
makerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
order: {
order: {
chainId: '1',
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
new BigNumber(fakeFiveMinutesLater.toString()),
new BigNumber(1),
new BigNumber(1),
).toString(),
maker: '0xmaker',
makerAmount: '1000000',
makerToken: '0xmakertoken',
taker: '0xtaker',
takerAmount: '10000000',
takerToken: '0xtakertoken',
txOrigin: '',
verifyingContract: '',
},
type: RfqmOrderTypes.Otc,
},
orderHash: '0x01234567',
status: RfqmJobStatus.PendingEnqueued,
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
updatedAt: new Date(),
workerAddress: '',
workflow: 'rfqm',
});
const mockDbUtils = mock(RfqmDbUtils);
when(
mockDbUtils.findV2TransactionSubmissionsByOrderHashAsync(
'0x01234567',
deepEqual([RfqmTransactionSubmissionType.Approval]),
),
).thenResolve([]);
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.generateApprovalCalldataAsync(anything(), anything(), anything())).thenResolve(
MOCK_EXECUTE_META_TRANSACTION_CALLDATA,
);
when(mockBlockchainUtils.estimateGasForAsync(anything())).thenResolve(10);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
rfqBlockchainUtils: instance(mockBlockchainUtils),
});
const calldata = await rfqmService.prepareApprovalAsync(
job,
'0xworkeraddress',
'0xtoken',
MOCK_EXECUTE_META_TRANSACTION_APPROVAL,
{
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
);
expect(calldata).to.deep.equal(MOCK_EXECUTE_META_TRANSACTION_CALLDATA);
});
it('should return generated calldata if there are submitted transactions for a meta-transaction job', async () => {
const jobId = 'jobId';
const transactionSubmissionId = 'submissionId';
const job = createMetaTransactionJobEntity(
{
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
fee: {
amount: new BigNumber(0),
token: '',
type: 'fixed',
},
inputToken: '0xinputToken',
inputTokenAmount: new BigNumber(10),
integratorId: '0xintegrator',
metaTransaction: MOCK_META_TRANSACTION,
metaTransactionHash: MOCK_META_TRANSACTION.getHash(),
minOutputTokenAmount: new BigNumber(10),
outputToken: '0xoutputToken',
takerAddress: '0xtakerAddress',
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
status: RfqmJobStatus.PendingEnqueued,
workerAddress: '0xworkeraddress',
},
jobId,
);
const mockTransaction = createMetaTransactionSubmissionEntity(
{
from: '0xworkeraddress',
metaTransactionJobId: jobId,
maxFeePerGas: new BigNumber(100000),
maxPriorityFeePerGas: new BigNumber(100),
nonce: 0,
to: '0xexchangeproxyaddress',
transactionHash: '0xsignedtransactionhash',
type: RfqmTransactionSubmissionType.Approval,
},
transactionSubmissionId,
);
const mockDbUtils = mock(RfqmDbUtils);
when(
mockDbUtils.findMetaTransactionSubmissionsByJobIdAsync(
jobId,
deepEqual([RfqmTransactionSubmissionType.Approval]),
),
).thenResolve([mockTransaction]);
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.generateApprovalCalldataAsync(anything(), anything(), anything())).thenResolve(
MOCK_EXECUTE_META_TRANSACTION_CALLDATA,
);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
rfqBlockchainUtils: instance(mockBlockchainUtils),
});
const calldata = await rfqmService.prepareApprovalAsync(
job,
'0xworkeraddress',
'0xtoken',
MOCK_EXECUTE_META_TRANSACTION_APPROVAL,
{
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
);
expect(calldata).to.deep.equal(MOCK_EXECUTE_META_TRANSACTION_CALLDATA);
verify(mockBlockchainUtils.estimateGasForAsync(anything())).never();
});
it('should throw exception if eth_call failed for a meta-transaction job', async () => {
const jobId = 'jobId';
const job = createMetaTransactionJobEntity(
{
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
fee: {
amount: new BigNumber(0),
token: '',
type: 'fixed',
},
inputToken: '0xinputToken',
inputTokenAmount: new BigNumber(10),
integratorId: '0xintegrator',
metaTransaction: MOCK_META_TRANSACTION,
metaTransactionHash: MOCK_META_TRANSACTION.getHash(),
minOutputTokenAmount: new BigNumber(10),
outputToken: '0xoutputToken',
takerAddress: '0xtakerAddress',
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
status: RfqmJobStatus.PendingEnqueued,
workerAddress: '0xworkeraddress',
},
jobId,
);
const mockDbUtils = mock(RfqmDbUtils);
when(
mockDbUtils.findMetaTransactionSubmissionsByJobIdAsync(
jobId,
deepEqual([RfqmTransactionSubmissionType.Approval]),
),
).thenResolve([]);
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.generateApprovalCalldataAsync(anything(), anything(), anything())).thenResolve(
MOCK_EXECUTE_META_TRANSACTION_CALLDATA,
);
when(mockBlockchainUtils.estimateGasForAsync(anything())).thenThrow(new Error('error'));
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
rfqBlockchainUtils: instance(mockBlockchainUtils),
});
try {
await rfqmService.prepareApprovalAsync(
job,
'0xworkeraddress',
'0xtoken',
MOCK_EXECUTE_META_TRANSACTION_APPROVAL,
{
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
);
expect.fail();
} catch (e) {
expect(e.message).to.contain('Eth call approval validation failed');
expect(job.status).to.deep.equal(RfqmJobStatus.FailedEthCallFailed);
}
});
it('should return correct calldata if there is no submitted transaction for a meta-transaction job', async () => {
const jobId = 'jobId';
const job = createMetaTransactionJobEntity(
{
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
fee: {
amount: new BigNumber(0),
token: '',
type: 'fixed',
},
inputToken: '0xinputToken',
inputTokenAmount: new BigNumber(10),
integratorId: '0xintegrator',
metaTransaction: MOCK_META_TRANSACTION,
metaTransactionHash: MOCK_META_TRANSACTION.getHash(),
minOutputTokenAmount: new BigNumber(10),
outputToken: '0xoutputToken',
takerAddress: '0xtakerAddress',
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
status: RfqmJobStatus.PendingEnqueued,
workerAddress: '0xworkeraddress',
},
jobId,
);
const mockDbUtils = mock(RfqmDbUtils);
when(
mockDbUtils.findMetaTransactionSubmissionsByJobIdAsync(
jobId,
deepEqual([RfqmTransactionSubmissionType.Approval]),
),
).thenResolve([]);
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.generateApprovalCalldataAsync(anything(), anything(), anything())).thenResolve(
MOCK_EXECUTE_META_TRANSACTION_CALLDATA,
);
when(mockBlockchainUtils.estimateGasForAsync(anything())).thenResolve(10);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
rfqBlockchainUtils: instance(mockBlockchainUtils),
});
const calldata = await rfqmService.prepareApprovalAsync(
job,
'0xworkeraddress',
'0xtoken',
MOCK_EXECUTE_META_TRANSACTION_APPROVAL,
{
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
);
expect(calldata).to.deep.equal(MOCK_EXECUTE_META_TRANSACTION_CALLDATA);
});
it('should return generated calldata if there are submitted transactions for a meta-transaction v2 job', async () => {
const jobId = 'jobId';
const transactionSubmissionId = 'submissionId';
const job = createMetaTransactionV2JobEntity(
{
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
inputToken: '0xinputToken',
inputTokenAmount: new BigNumber(10),
integratorId: '0xintegrator',
calledFunction: 'transformERC20',
metaTransaction: MOCK_META_TRANSACTION_V2,
metaTransactionHash: MOCK_META_TRANSACTION_V2.getHash(),
minOutputTokenAmount: new BigNumber(10),
outputToken: '0xoutputToken',
takerAddress: '0xtakerAddress',
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
tokens: ['0xinputToken', '0xoutputToken'],
status: RfqmJobStatus.PendingEnqueued,
workerAddress: '0xworkeraddress',
},
jobId,
);
const mockTransaction = createMetaTransactionV2SubmissionEntity(
{
from: '0xworkeraddress',
metaTransactionV2JobId: jobId,
maxFeePerGas: new BigNumber(100000),
maxPriorityFeePerGas: new BigNumber(100),
nonce: 0,
to: '0xexchangeproxyaddress',
transactionHash: '0xsignedtransactionhash',
type: RfqmTransactionSubmissionType.Approval,
},
transactionSubmissionId,
);
const mockDbUtils = mock(RfqmDbUtils);
when(
mockDbUtils.findMetaTransactionV2SubmissionsByJobIdAsync(
jobId,
deepEqual([RfqmTransactionSubmissionType.Approval]),
),
).thenResolve([mockTransaction]);
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.generateApprovalCalldataAsync(anything(), anything(), anything())).thenResolve(
MOCK_EXECUTE_META_TRANSACTION_CALLDATA,
);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
rfqBlockchainUtils: instance(mockBlockchainUtils),
});
const calldata = await rfqmService.prepareApprovalAsync(
job,
'0xworkeraddress',
'0xtoken',
MOCK_EXECUTE_META_TRANSACTION_APPROVAL,
{
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
);
expect(calldata).to.deep.equal(MOCK_EXECUTE_META_TRANSACTION_CALLDATA);
verify(mockBlockchainUtils.estimateGasForAsync(anything())).never();
});
it('should throw exception if eth_call failed for a meta-transaction v2 job', async () => {
const jobId = 'jobId';
const job = createMetaTransactionV2JobEntity(
{
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
inputToken: '0xinputToken',
inputTokenAmount: new BigNumber(10),
integratorId: '0xintegrator',
calledFunction: 'transformERC20',
metaTransaction: MOCK_META_TRANSACTION_V2,
metaTransactionHash: MOCK_META_TRANSACTION_V2.getHash(),
minOutputTokenAmount: new BigNumber(10),
outputToken: '0xoutputToken',
takerAddress: '0xtakerAddress',
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
tokens: ['0xinputToken', '0xoutputToken'],
status: RfqmJobStatus.PendingEnqueued,
workerAddress: '0xworkeraddress',
},
jobId,
);
const mockDbUtils = mock(RfqmDbUtils);
when(
mockDbUtils.findMetaTransactionV2SubmissionsByJobIdAsync(
jobId,
deepEqual([RfqmTransactionSubmissionType.Approval]),
),
).thenResolve([]);
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.generateApprovalCalldataAsync(anything(), anything(), anything())).thenResolve(
MOCK_EXECUTE_META_TRANSACTION_CALLDATA,
);
when(mockBlockchainUtils.estimateGasForAsync(anything())).thenThrow(new Error('error'));
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
rfqBlockchainUtils: instance(mockBlockchainUtils),
});
try {
await rfqmService.prepareApprovalAsync(
job,
'0xworkeraddress',
'0xtoken',
MOCK_EXECUTE_META_TRANSACTION_APPROVAL,
{
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
);
expect.fail();
} catch (e) {
expect(e.message).to.contain('Eth call approval validation failed');
expect(job.status).to.deep.equal(RfqmJobStatus.FailedEthCallFailed);
}
});
it('should return correct calldata if there is no submitted transaction for a meta-transaction v2 job', async () => {
const jobId = 'jobId';
const job = createMetaTransactionV2JobEntity(
{
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
inputToken: '0xinputToken',
inputTokenAmount: new BigNumber(10),
integratorId: '0xintegrator',
calledFunction: 'transformERC20',
metaTransaction: MOCK_META_TRANSACTION_V2,
metaTransactionHash: MOCK_META_TRANSACTION_V2.getHash(),
minOutputTokenAmount: new BigNumber(10),
outputToken: '0xoutputToken',
takerAddress: '0xtakerAddress',
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
tokens: ['0xinputToken', '0xoutputToken'],
status: RfqmJobStatus.PendingEnqueued,
workerAddress: '0xworkeraddress',
},
jobId,
);
const mockDbUtils = mock(RfqmDbUtils);
when(
mockDbUtils.findMetaTransactionV2SubmissionsByJobIdAsync(
jobId,
deepEqual([RfqmTransactionSubmissionType.Approval]),
),
).thenResolve([]);
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.generateApprovalCalldataAsync(anything(), anything(), anything())).thenResolve(
MOCK_EXECUTE_META_TRANSACTION_CALLDATA,
);
when(mockBlockchainUtils.estimateGasForAsync(anything())).thenResolve(10);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
rfqBlockchainUtils: instance(mockBlockchainUtils),
});
const calldata = await rfqmService.prepareApprovalAsync(
job,
'0xworkeraddress',
'0xtoken',
MOCK_EXECUTE_META_TRANSACTION_APPROVAL,
{
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
);
expect(calldata).to.deep.equal(MOCK_EXECUTE_META_TRANSACTION_CALLDATA);
});
});
describe('prepareRfqmV2TradeAsync', () => {
it('updates the job and throws upon validation failure when `shouldCheckLastLook` is true', async () => {
const expiredJob = new RfqmV2JobEntity({
affiliateAddress: '',
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeOneMinuteAgoS),
fee: {
amount: '0',
token: '',
type: 'fixed',
},
integratorId: '',
lastLookResult: null,
makerUri: 'http://foo.bar',
order: {
order: {
chainId: '1',
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
new BigNumber(fakeOneMinuteAgoS.toString()),
new BigNumber(1),
new BigNumber(1),
).toString(),
maker: '',
makerAmount: '',
makerToken: '',
taker: '',
takerAmount: '',
takerToken: '',
txOrigin: '',
verifyingContract: '',
},
type: RfqmOrderTypes.Otc,
},
orderHash: '0x01234567',
status: RfqmJobStatus.PendingEnqueued,
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
updatedAt: new Date(),
workerAddress: '',
workflow: 'rfqm',
});
const _job = _.cloneDeep(expiredJob);
const mockDbUtils = mock(RfqmDbUtils);
when(mockDbUtils.findV2TransactionSubmissionsByOrderHashAsync('0x01234567')).thenResolve([]);
const rfqmService = buildWorkerServiceForUnitTest({ dbUtils: instance(mockDbUtils) });
try {
await rfqmService.prepareRfqmV2TradeAsync(
expiredJob,
'0xworkeraddress',
{ shouldSimulate: true, shouldCheckLastLook: true },
new Date(fakeClockMs),
);
expect.fail();
} catch (e) {
expect(e.message).to.contain('Job failed validation');
expect(expiredJob).to.deep.equal({ ..._job, status: RfqmJobStatus.FailedExpired });
}
});
it('handles a balance check failure', async () => {
const job = new RfqmV2JobEntity({
affiliateAddress: '',
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
fee: {
amount: '0',
token: '',
type: 'fixed',
},
integratorId: '',
lastLookResult: null,
makerUri: 'http://foo.bar',
makerSignature: null,
order: {
order: {
chainId: '1',
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
new BigNumber(fakeFiveMinutesLater.toString()),
new BigNumber(1),
new BigNumber(1),
).toString(),
maker: '0xmaker',
makerAmount: '1000000',
makerToken: '0xmakertoken',
taker: '0xtaker',
takerAmount: '10000000',
takerToken: '0xtakertoken',
txOrigin: '',
verifyingContract: '',
},
type: RfqmOrderTypes.Otc,
},
orderHash: '0x01234567',
status: RfqmJobStatus.PendingEnqueued,
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
updatedAt: new Date(),
workerAddress: '',
workflow: 'rfqm',
});
const _job = _.cloneDeep(job);
const mockDbUtils = mock(RfqmDbUtils);
when(mockDbUtils.findV2TransactionSubmissionsByOrderHashAsync('0x01234567')).thenResolve([]);
const updateRfqmJobCalledArgs: RfqmJobEntity[] = [];
when(mockDbUtils.updateRfqmJobAsync(anything())).thenCall(async (jobArg) => {
updateRfqmJobCalledArgs.push(_.cloneDeep(jobArg));
});
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.getMinOfBalancesAndAllowancesAsync(anything())).thenResolve([new BigNumber(100)]);
const mockRfqMakerBalanceCacheService = mock(RfqMakerBalanceCacheService);
when(mockRfqMakerBalanceCacheService.getERC20OwnerBalancesAsync(anything(), anything())).thenResolve([
new BigNumber(5),
]);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
rfqBlockchainUtils: instance(mockBlockchainUtils),
rfqMakerBalanceCacheService: instance(mockRfqMakerBalanceCacheService),
});
try {
await rfqmService.prepareRfqmV2TradeAsync(
job,
'0xworkeraddress',
{ shouldSimulate: true, shouldCheckLastLook: true },
new Date(fakeClockMs),
);
expect.fail();
} catch (e) {
expect(e.message).to.contain('Order failed pre-sign validation');
expect(updateRfqmJobCalledArgs[0]).to.deep.equal({
..._job,
status: RfqmJobStatus.PendingProcessing,
});
expect(updateRfqmJobCalledArgs[1]).to.deep.equal({
..._job,
status: RfqmJobStatus.FailedPresignValidationFailed,
});
expect(job).to.deep.equal({
..._job,
status: RfqmJobStatus.FailedPresignValidationFailed,
});
}
});
it('handles a decline to sign', async () => {
const job = new RfqmV2JobEntity({
affiliateAddress: '',
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
fee: {
amount: '0',
token: '',
type: 'fixed',
},
integratorId: '',
lastLookResult: null,
makerUri: 'http://foo.bar',
makerSignature: null,
order: {
order: {
chainId: '1',
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
new BigNumber(fakeFiveMinutesLater.toString()),
new BigNumber(1),
new BigNumber(1),
).toString(),
maker: '0xmaker',
makerAmount: '1000000',
makerToken: '0xmakertoken',
taker: '0xtaker',
takerAmount: '10000000',
takerToken: '0xtakertoken',
txOrigin: '',
verifyingContract: '',
},
type: RfqmOrderTypes.Otc,
},
orderHash: '0x01234567',
status: RfqmJobStatus.PendingEnqueued,
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
updatedAt: new Date(),
workerAddress: '',
workflow: 'rfqm',
});
const _job = _.cloneDeep(job);
const mockDbUtils = mock(RfqmDbUtils);
when(mockDbUtils.findV2TransactionSubmissionsByOrderHashAsync('0x01234567')).thenResolve([]);
const updateRfqmJobCalledArgs: RfqmJobEntity[] = [];
when(mockDbUtils.updateRfqmJobAsync(anything())).thenCall(async (jobArg) => {
updateRfqmJobCalledArgs.push(_.cloneDeep(jobArg));
});
const mockQuoteServerClient = mock(QuoteServerClient);
when(mockQuoteServerClient.signV2Async(anything(), anything(), anything())).thenResolve(undefined);
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.getMinOfBalancesAndAllowancesAsync(anything())).thenResolve([
new BigNumber(1000000000),
]);
const mockRfqMakerBalanceCacheService = mock(RfqMakerBalanceCacheService);
when(mockRfqMakerBalanceCacheService.getERC20OwnerBalancesAsync(anything(), anything())).thenResolve([
new BigNumber(1000000000),
]);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
quoteServerClient: instance(mockQuoteServerClient),
rfqBlockchainUtils: instance(mockBlockchainUtils),
rfqMakerBalanceCacheService: instance(mockRfqMakerBalanceCacheService),
});
try {
await rfqmService.prepareRfqmV2TradeAsync(
job,
'0xworkeraddress',
{ shouldSimulate: true, shouldCheckLastLook: true },
new Date(fakeClockMs),
);
expect.fail();
} catch (e) {
expect(e.message).to.contain('Market Maker declined to sign');
expect(job).to.deep.equal({
..._job,
lastLookResult: false,
status: RfqmJobStatus.FailedLastLookDeclined,
});
}
});
it('handles a signature failure', async () => {
const job = new RfqmV2JobEntity({
affiliateAddress: '',
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
fee: {
amount: '0',
token: '',
type: 'fixed',
},
integratorId: '',
lastLookResult: null,
makerUri: 'http://foo.bar',
makerSignature: null,
order: {
order: {
chainId: '1',
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
new BigNumber(fakeFiveMinutesLater.toString()),
new BigNumber(1),
new BigNumber(1),
).toString(),
maker: '0xmaker',
makerAmount: '1000000',
makerToken: '0xmakertoken',
taker: '0xtaker',
takerAmount: '10000000',
takerToken: '0xtakertoken',
txOrigin: '',
verifyingContract: '',
},
type: RfqmOrderTypes.Otc,
},
orderHash: '0x01234567',
status: RfqmJobStatus.PendingEnqueued,
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
updatedAt: new Date(),
workerAddress: '',
workflow: 'rfqm',
});
const _job = _.cloneDeep(job);
const mockDbUtils = mock(RfqmDbUtils);
when(mockDbUtils.findV2TransactionSubmissionsByOrderHashAsync('0x01234567')).thenResolve([]);
const mockQuoteServerClient = mock(QuoteServerClient);
when(mockQuoteServerClient.signV2Async(anything(), anything(), anything())).thenReject(
new Error('fake timeout'),
);
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.getMinOfBalancesAndAllowancesAsync(anything())).thenResolve([
new BigNumber(1000000000),
]);
const mockRfqMakerBalanceCacheService = mock(RfqMakerBalanceCacheService);
when(mockRfqMakerBalanceCacheService.getERC20OwnerBalancesAsync(anything(), anything())).thenResolve([
new BigNumber(1000000000),
]);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
quoteServerClient: instance(mockQuoteServerClient),
rfqBlockchainUtils: instance(mockBlockchainUtils),
rfqMakerBalanceCacheService: instance(mockRfqMakerBalanceCacheService),
});
try {
await rfqmService.prepareRfqmV2TradeAsync(
job,
'0xworkeraddress',
{ shouldSimulate: true, shouldCheckLastLook: true },
new Date(fakeClockMs),
);
expect.fail();
} catch (e) {
expect(e.message).to.contain('Job failed during market maker sign attempt');
expect(job).to.deep.equal({
..._job,
status: RfqmJobStatus.FailedSignFailed,
});
}
});
it('handles signer is not the maker', async () => {
const job = new RfqmV2JobEntity({
affiliateAddress: '',
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
fee: {
amount: '0',
token: '',
type: 'fixed',
},
integratorId: '',
lastLookResult: null,
makerUri: 'http://foo.bar',
makerSignature: null,
order: {
order: {
chainId: '1',
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
new BigNumber(fakeFiveMinutesLater.toString()),
new BigNumber(1),
new BigNumber(1),
).toString(),
maker,
makerAmount: '1000000',
makerToken: '0xmakertoken',
taker: '0xtaker',
takerAmount: '10000000',
takerToken: '0xtakertoken',
txOrigin: '',
verifyingContract: '',
},
type: RfqmOrderTypes.Otc,
},
orderHash,
status: RfqmJobStatus.PendingEnqueued,
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
updatedAt: new Date(),
workerAddress: '',
workflow: 'rfqm',
});
const mockDbUtils = mock(RfqmDbUtils);
const updateRfqmJobCalledArgs: RfqmJobEntity[] = [];
when(mockDbUtils.updateRfqmJobAsync(anything())).thenCall(async (jobArg) => {
updateRfqmJobCalledArgs.push(_.cloneDeep(jobArg));
});
when(mockDbUtils.findV2TransactionSubmissionsByOrderHashAsync(orderHash)).thenResolve([]);
const mockQuoteServerClient = mock(QuoteServerClient);
const invalidEIP712Sig = _.cloneDeep(validEIP712Sig);
invalidEIP712Sig.r = '0xdc158f7b53b940863bc7b001552a90282e51033f29b73d44a2701bd16faa19d3';
when(mockQuoteServerClient.signV2Async(anything(), anything(), anything())).thenResolve(invalidEIP712Sig);
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.getMinOfBalancesAndAllowancesAsync(anything())).thenResolve([
new BigNumber(1000000000),
]);
const mockRfqMakerBalanceCacheService = mock(RfqMakerBalanceCacheService);
when(mockRfqMakerBalanceCacheService.getERC20OwnerBalancesAsync(anything(), anything())).thenResolve([
new BigNumber(1000000000),
]);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
quoteServerClient: instance(mockQuoteServerClient),
rfqBlockchainUtils: instance(mockBlockchainUtils),
rfqMakerBalanceCacheService: instance(mockRfqMakerBalanceCacheService),
});
try {
await rfqmService.prepareRfqmV2TradeAsync(
job,
'0xworkeraddress',
{ shouldSimulate: true, shouldCheckLastLook: true },
new Date(fakeClockMs),
);
expect.fail();
} catch (e) {
expect(e.message).to.contain('Invalid order signer address');
expect(job.status).to.deep.equal(RfqmJobStatus.FailedSignFailed);
}
});
it('handles an eth_call failure', async () => {
const job = new RfqmV2JobEntity({
affiliateAddress: '',
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
fee: {
amount: '0',
token: '',
type: 'fixed',
},
integratorId: '',
lastLookResult: null,
makerUri: 'http://foo.bar',
makerSignature: null,
order: {
order: {
chainId: '1',
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
new BigNumber(fakeFiveMinutesLater.toString()),
new BigNumber(1),
new BigNumber(1),
).toString(),
maker,
makerAmount: '1000000',
makerToken: '0xmakertoken',
taker: '0xtaker',
takerAmount: '10000000',
takerToken: '0xtakertoken',
txOrigin: '',
verifyingContract: '',
},
type: RfqmOrderTypes.Otc,
},
orderHash,
status: RfqmJobStatus.PendingEnqueued,
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
updatedAt: new Date(),
workerAddress: '',
workflow: 'rfqm',
});
const _job = _.cloneDeep(job);
const mockDbUtils = mock(RfqmDbUtils);
when(mockDbUtils.findV2TransactionSubmissionsByOrderHashAsync(orderHash)).thenResolve([]);
const mockQuoteServerClient = mock(QuoteServerClient);
when(mockQuoteServerClient.signV2Async(anything(), anything(), anything())).thenResolve(validEIP712Sig);
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.getMinOfBalancesAndAllowancesAsync(anything())).thenResolve([
new BigNumber(1000000000),
]);
when(mockBlockchainUtils.estimateGasForAsync(anything())).thenReject(new Error('fake eth call failure'));
const mockRfqMakerBalanceCacheService = mock(RfqMakerBalanceCacheService);
when(mockRfqMakerBalanceCacheService.getERC20OwnerBalancesAsync(anything(), anything())).thenResolve([
new BigNumber(1000000000),
]);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
quoteServerClient: instance(mockQuoteServerClient),
rfqBlockchainUtils: instance(mockBlockchainUtils),
rfqMakerBalanceCacheService: instance(mockRfqMakerBalanceCacheService),
});
try {
await rfqmService.prepareRfqmV2TradeAsync(
job,
'0xworkeraddress',
{ shouldSimulate: true, shouldCheckLastLook: true },
new Date(fakeClockMs),
);
expect.fail();
} catch (e) {
expect(e.message).to.contain('Eth call validation failed');
expect(job).to.deep.equal({
..._job,
lastLookResult: true,
makerSignature: validEIP712Sig,
status: RfqmJobStatus.FailedEthCallFailed,
});
}
});
it('updates market maker signatures missing bytes', async () => {
const job = new RfqmV2JobEntity({
affiliateAddress: '',
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
fee: {
amount: '0',
token: '',
type: 'fixed',
},
integratorId: '',
lastLookResult: null,
makerUri: 'http://foo.bar',
makerSignature: null,
order: {
order: {
chainId: '1',
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
new BigNumber(fakeFiveMinutesLater.toString()),
new BigNumber(1),
new BigNumber(1),
).toString(),
maker,
makerAmount: '1000000',
makerToken: '0xmakertoken',
taker: '0xtaker',
takerAmount: '10000000',
takerToken: '0xtakertoken',
txOrigin: '',
verifyingContract: '',
},
type: RfqmOrderTypes.Otc,
},
orderHash,
status: RfqmJobStatus.PendingEnqueued,
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
updatedAt: new Date(),
workerAddress: '',
workflow: 'rfqm',
});
const _job = _.cloneDeep(job);
const mockDbUtils = mock(RfqmDbUtils);
when(mockDbUtils.findV2TransactionSubmissionsByOrderHashAsync(anything())).thenResolve([]);
const mockQuoteServerClient = mock(QuoteServerClient);
when(mockQuoteServerClient.signV2Async(anything(), anything(), anything())).thenResolve(missingByteSig);
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.isValidOrderSignerAsync(anything(), anything())).thenResolve(true);
when(mockBlockchainUtils.getMinOfBalancesAndAllowancesAsync(anything())).thenResolve([
new BigNumber(1000000000),
]);
when(mockBlockchainUtils.estimateGasForAsync(anything())).thenResolve(0);
when(
mockBlockchainUtils.generateTakerSignedOtcOrderCallData(
anything(),
anything(),
anything(),
anything(),
anything(),
),
).thenReturn('0xvalidcalldata');
const mockRfqMakerBalanceCacheService = mock(RfqMakerBalanceCacheService);
when(mockRfqMakerBalanceCacheService.getERC20OwnerBalancesAsync(anything(), anything())).thenResolve([
new BigNumber(1000000000),
]);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
quoteServerClient: instance(mockQuoteServerClient),
rfqBlockchainUtils: instance(mockBlockchainUtils),
rfqMakerBalanceCacheService: instance(mockRfqMakerBalanceCacheService),
});
await rfqmService.prepareRfqmV2TradeAsync(
job,
'0xworkeraddress',
{ shouldSimulate: true, shouldCheckLastLook: true },
new Date(fakeClockMs),
);
expect(job).to.deep.equal({
..._job,
lastLookResult: true,
makerSignature: padSignature(missingByteSig),
status: RfqmJobStatus.PendingLastLookAccepted,
});
});
it('skips the eth_call for jobs with existing submissions', async () => {
const job = new RfqmV2JobEntity({
affiliateAddress: '',
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
fee: {
amount: '0',
token: '',
type: 'fixed',
},
integratorId: '',
lastLookResult: true,
makerUri: 'http://foo.bar',
makerSignature: validEIP712Sig,
order: {
order: {
chainId: '1',
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
new BigNumber(fakeFiveMinutesLater.toString()),
new BigNumber(1),
new BigNumber(1),
).toString(),
maker: '0xmaker',
makerAmount: '1000000',
makerToken: '0xmakertoken',
taker: '0xtaker',
takerAmount: '10000000',
takerToken: '0xtakertoken',
txOrigin: '',
verifyingContract: '',
},
type: RfqmOrderTypes.Otc,
},
orderHash: '0x01234567',
status: RfqmJobStatus.PendingLastLookAccepted,
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
updatedAt: new Date(),
workerAddress: '',
workflow: 'rfqm',
});
const transaction = new RfqmV2TransactionSubmissionEntity({
orderHash,
to: '0xexchangeproxyaddress',
from: '0xworkeraddress',
transactionHash: '0xsignedtransactionhash',
maxFeePerGas: new BigNumber(100000),
maxPriorityFeePerGas: new BigNumber(100),
nonce: 21,
type: RfqmTransactionSubmissionType.Trade,
});
const _job = _.cloneDeep(job);
const mockDbUtils = mock(RfqmDbUtils);
const updateRfqmJobCalledArgs: RfqmJobEntity[] = [];
when(mockDbUtils.findV2TransactionSubmissionsByOrderHashAsync('0x01234567')).thenResolve([transaction]);
when(mockDbUtils.updateRfqmJobAsync(anything())).thenCall(async (jobArg) => {
updateRfqmJobCalledArgs.push(_.cloneDeep(jobArg));
});
const mockQuoteServerClient = mock(QuoteServerClient);
when(mockQuoteServerClient.signV2Async(anything(), anything(), anything())).thenResolve(validEIP712Sig);
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.getMinOfBalancesAndAllowancesAsync(anything())).thenResolve([
new BigNumber(1000000000),
]);
when(
mockBlockchainUtils.generateTakerSignedOtcOrderCallData(
anything(),
anything(),
anything(),
anything(),
anything(),
),
).thenReturn('0xvalidcalldata');
const mockRfqMakerBalanceCacheService = mock(RfqMakerBalanceCacheService);
when(mockRfqMakerBalanceCacheService.getERC20OwnerBalancesAsync(anything(), anything())).thenResolve([
new BigNumber(1000000000),
]);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
quoteServerClient: instance(mockQuoteServerClient),
rfqBlockchainUtils: instance(mockBlockchainUtils),
rfqMakerBalanceCacheService: instance(mockRfqMakerBalanceCacheService),
});
const calldata = await rfqmService.prepareRfqmV2TradeAsync(
job,
'0xworkeraddress',
{ shouldSimulate: true, shouldCheckLastLook: true },
new Date(fakeClockMs),
);
expect(job).to.deep.equal({
..._job,
lastLookResult: true,
makerSignature: validEIP712Sig,
status: RfqmJobStatus.PendingLastLookAccepted,
});
expect(calldata).to.equal('0xvalidcalldata');
verify(mockBlockchainUtils.estimateGasForAsync(anything())).never();
});
it('lets expired jobs with existing submissions fall through', async () => {
// If the job isn't in a terminal status but there are existing submissions,
// `prepareTradeAsync` will let the job continue to the submission step which
// will allow the worker to check receipts for those submissions.
const expiredJob = new RfqmV2JobEntity({
affiliateAddress: '',
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeOneMinuteAgoS),
fee: {
amount: '0',
token: '',
type: 'fixed',
},
integratorId: '',
lastLookResult: true,
makerUri: 'http://foo.bar',
makerSignature: {
r: '0x01',
s: '0x02',
signatureType: SignatureType.EthSign,
v: 27,
},
order: {
order: {
chainId: '1',
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
new BigNumber(fakeOneMinuteAgoS.toString()),
new BigNumber(1),
new BigNumber(1),
).toString(),
maker: '0xmaker',
makerAmount: '1000000',
makerToken: '0xmakertoken',
taker: '0xtaker',
takerAmount: '10000000',
takerToken: '0xtakertoken',
txOrigin: '',
verifyingContract: '',
},
type: RfqmOrderTypes.Otc,
},
orderHash: '0x01234567',
status: RfqmJobStatus.PendingSubmitted,
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
updatedAt: new Date(),
workerAddress: '',
workflow: 'rfqm',
});
const transaction = new RfqmV2TransactionSubmissionEntity({
orderHash: '0x01234567',
to: '0xexchangeproxyaddress',
from: '0xworkeraddress',
transactionHash: '0xsignedtransactionhash',
maxFeePerGas: new BigNumber(100000),
maxPriorityFeePerGas: new BigNumber(100),
nonce: 21,
type: RfqmTransactionSubmissionType.Trade,
});
const mockDbUtils = mock(RfqmDbUtils);
when(mockDbUtils.findV2TransactionSubmissionsByOrderHashAsync('0x01234567')).thenResolve([transaction]);
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(
mockBlockchainUtils.generateTakerSignedOtcOrderCallData(
anything(),
anything(),
anything(),
anything(),
anything(),
),
).thenReturn('0xvalidcalldata');
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
rfqBlockchainUtils: instance(mockBlockchainUtils),
});
await rfqmService.prepareRfqmV2TradeAsync(
expiredJob,
'0xworkeraddress',
{ shouldSimulate: true, shouldCheckLastLook: true },
new Date(fakeClockMs),
);
expect(expiredJob.status).to.equal(RfqmJobStatus.PendingSubmitted);
});
it('successfully prepares a job when checking last look', async () => {
const job = new RfqmV2JobEntity({
affiliateAddress: '',
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
fee: {
amount: '0',
token: '',
type: 'fixed',
},
integratorId: '',
lastLookResult: null,
makerUri: 'http://foo.bar',
makerSignature: null,
order: {
order: {
chainId: '1',
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
new BigNumber(fakeFiveMinutesLater.toString()),
new BigNumber(1),
new BigNumber(1),
).toString(),
maker,
makerAmount: '1000000',
makerToken: '0xmakertoken',
taker: '0xtaker',
takerAmount: '10000000',
takerToken: '0xtakertoken',
txOrigin: '',
verifyingContract: '',
},
type: RfqmOrderTypes.Otc,
},
orderHash,
status: RfqmJobStatus.PendingEnqueued,
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
updatedAt: new Date(),
workerAddress: '0xworkeraddress',
workflow: 'rfqm',
});
const _job = _.cloneDeep(job);
const mockDbUtils = mock(RfqmDbUtils);
const updateRfqmJobCalledArgs: RfqmJobEntity[] = [];
when(mockDbUtils.findV2TransactionSubmissionsByOrderHashAsync(orderHash)).thenResolve([]);
when(mockDbUtils.updateRfqmJobAsync(anything())).thenCall(async (jobArg) => {
updateRfqmJobCalledArgs.push(_.cloneDeep(jobArg));
});
const mockQuoteServerClient = mock(QuoteServerClient);
when(mockQuoteServerClient.signV2Async(anything(), anything(), anything())).thenResolve(validEIP712Sig);
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.getMinOfBalancesAndAllowancesAsync(anything())).thenResolve([
new BigNumber(1000000000),
]);
when(mockBlockchainUtils.estimateGasForAsync(anything())).thenResolve(0);
when(
mockBlockchainUtils.generateTakerSignedOtcOrderCallData(
anything(),
anything(),
anything(),
anything(),
anything(),
),
).thenReturn('0xvalidcalldata');
const mockRfqMakerBalanceCacheService = mock(RfqMakerBalanceCacheService);
when(mockRfqMakerBalanceCacheService.getERC20OwnerBalancesAsync(anything(), anything())).thenResolve([
new BigNumber(1000000000),
]);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
quoteServerClient: instance(mockQuoteServerClient),
rfqBlockchainUtils: instance(mockBlockchainUtils),
rfqMakerBalanceCacheService: instance(mockRfqMakerBalanceCacheService),
});
const calldata = await rfqmService.prepareRfqmV2TradeAsync(
job,
'0xworkeraddress',
{ shouldSimulate: true, shouldCheckLastLook: true },
new Date(fakeClockMs),
);
expect(job).to.deep.equal({
..._job,
lastLookResult: true,
makerSignature: validEIP712Sig,
status: RfqmJobStatus.PendingLastLookAccepted,
});
expect(calldata).to.equal('0xvalidcalldata');
expect(updateRfqmJobCalledArgs[0]).to.deep.equal({
..._job,
status: RfqmJobStatus.PendingProcessing,
});
expect(updateRfqmJobCalledArgs[1]).to.deep.equal({
..._job,
lastLookResult: true,
makerSignature: validEIP712Sig,
status: RfqmJobStatus.PendingLastLookAccepted,
});
});
it('successfully prepares a job if no last look is necessary', async () => {
const job = new RfqmV2JobEntity({
affiliateAddress: '',
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
fee: {
amount: '0',
token: '',
type: 'fixed',
},
integratorId: '',
lastLookResult: true,
makerUri: 'http://foo.bar',
makerSignature: validEIP712Sig,
order: {
order: {
chainId: '1',
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
new BigNumber(fakeFiveMinutesLater.toString()),
new BigNumber(1),
new BigNumber(1),
).toString(),
maker,
makerAmount: '1000000',
makerToken: '0xmakertoken',
taker: '0xtaker',
takerAmount: '10000000',
takerToken: '0xtakertoken',
txOrigin: '',
verifyingContract: '',
},
type: RfqmOrderTypes.Otc,
},
orderHash,
status: RfqmJobStatus.PendingLastLookAccepted,
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
updatedAt: new Date(),
workerAddress: '0xworkeraddress',
workflow: 'rfqm',
});
const _job = _.cloneDeep(job);
const mockDbUtils = mock(RfqmDbUtils);
when(mockDbUtils.findV2TransactionSubmissionsByOrderHashAsync(orderHash)).thenResolve([]);
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.getMinOfBalancesAndAllowancesAsync(anything())).thenResolve([
new BigNumber(1000000000),
]);
when(mockBlockchainUtils.estimateGasForAsync(anything())).thenResolve(0);
when(
mockBlockchainUtils.generateTakerSignedOtcOrderCallData(
anything(),
anything(),
anything(),
anything(),
anything(),
),
).thenReturn('0xvalidcalldata');
const mockRfqMakerBalanceCacheService = mock(RfqMakerBalanceCacheService);
when(mockRfqMakerBalanceCacheService.getERC20OwnerBalancesAsync(anything(), anything())).thenResolve([
new BigNumber(1000000000),
]);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
rfqBlockchainUtils: instance(mockBlockchainUtils),
rfqMakerBalanceCacheService: instance(mockRfqMakerBalanceCacheService),
});
const spiedRfqmService = spy(rfqmService);
const calldata = await rfqmService.prepareRfqmV2TradeAsync(
job,
'0xworkeraddress',
{ shouldSimulate: true, shouldCheckLastLook: false },
new Date(fakeClockMs),
);
expect(job).to.deep.equal(_job);
expect(calldata).to.equal('0xvalidcalldata');
verify(spiedRfqmService.checkJobPreprocessingAsync(anything(), anything())).never();
verify(spiedRfqmService.checkLastLookAsync(anything(), anything(), anything())).never();
});
describe('Gasless RFQt VIP', () => {
it('handles a balance check failure', async () => {
const job = new RfqmV2JobEntity({
affiliateAddress: '',
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
fee: {
amount: '0',
token: '',
type: 'fixed',
},
integratorId: '',
lastLookResult: null,
makerUri: 'http://foo.bar',
makerSignature: validEIP712Sig,
order: {
order: {
chainId: '1',
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
new BigNumber(fakeFiveMinutesLater.toString()),
new BigNumber(1),
new BigNumber(1),
).toString(),
maker: '0xmaker',
makerAmount: '1000000',
makerToken: '0xmakertoken',
taker: '0xtaker',
takerAmount: '10000000',
takerToken: '0xtakertoken',
txOrigin: '',
verifyingContract: '',
},
type: RfqmOrderTypes.Otc,
},
orderHash: '0x01234567',
status: RfqmJobStatus.PendingEnqueued,
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
updatedAt: new Date(),
workerAddress: '',
workflow: 'gasless-rfqt',
});
const _job = _.cloneDeep(job);
const mockDbUtils = mock(RfqmDbUtils);
when(mockDbUtils.findV2TransactionSubmissionsByOrderHashAsync('0x01234567')).thenResolve([]);
const updateRfqmJobCalledArgs: RfqmJobEntity[] = [];
when(mockDbUtils.updateRfqmJobAsync(anything())).thenCall(async (jobArg) => {
updateRfqmJobCalledArgs.push(_.cloneDeep(jobArg));
});
const mockQuoteServerClient = mock(QuoteServerClient);
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.getMinOfBalancesAndAllowancesAsync(anything())).thenResolve([
new BigNumber(100),
]);
const mockRfqMakerBalanceCacheService = mock(RfqMakerBalanceCacheService);
when(mockRfqMakerBalanceCacheService.getERC20OwnerBalancesAsync(anything(), anything())).thenResolve([
new BigNumber(5),
]);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
quoteServerClient: instance(mockQuoteServerClient),
rfqBlockchainUtils: instance(mockBlockchainUtils),
rfqMakerBalanceCacheService: instance(mockRfqMakerBalanceCacheService),
});
try {
await rfqmService.prepareRfqmV2TradeAsync(
job,
'0xworkeraddress',
{ shouldSimulate: true, shouldCheckLastLook: true },
new Date(fakeClockMs),
);
expect.fail();
} catch (e) {
expect(e.message).to.contain('Order failed pre-submit validation');
verify(mockQuoteServerClient.signV2Async(anything(), anything(), anything())).never();
expect(updateRfqmJobCalledArgs[0]).to.deep.equal({
..._job,
status: RfqmJobStatus.PendingProcessing,
});
expect(updateRfqmJobCalledArgs[1]).to.deep.equal({
..._job,
status: RfqmJobStatus.FailedPresubmitValidationFailed,
});
expect(job).to.deep.equal({
..._job,
status: RfqmJobStatus.FailedPresubmitValidationFailed,
});
}
});
});
});
describe('prepareMetaTransactionTradeAsync', () => {
it('updates the job and throws upon validation failure if `shouldValidateJob` is true for meta-transaction v1 job', async () => {
const jobId = 'jobId';
const expiredJob = createMetaTransactionJobEntity(
{
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeOneMinuteAgoS),
fee: {
amount: new BigNumber(0),
token: '',
type: 'fixed',
},
inputToken: '0xinputToken',
inputTokenAmount: new BigNumber(10),
integratorId: '0xintegrator',
metaTransaction: MOCK_META_TRANSACTION,
metaTransactionHash: MOCK_META_TRANSACTION.getHash(),
minOutputTokenAmount: new BigNumber(10),
outputToken: '0xoutputToken',
takerAddress: '0xtakerAddress',
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
status: RfqmJobStatus.PendingEnqueued,
workerAddress: '0xworkeraddress',
},
jobId,
);
const _job = _.cloneDeep(expiredJob);
const gasStationAttendantMock = mock(GasStationAttendantEthereum);
when(gasStationAttendantMock.getExpectedTransactionGasRateAsync()).thenResolve(
new BigNumber(10).shiftedBy(GWEI_DECIMALS),
);
const mockDbUtils = mock(RfqmDbUtils);
when(mockDbUtils.findMetaTransactionSubmissionsByJobIdAsync(jobId)).thenResolve([]);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
gasStationAttendant: instance(gasStationAttendantMock),
});
try {
await rfqmService.prepareMetaTransactionTradeAsync(
expiredJob,
'0xworkeraddress',
{ shouldValidateJob: true, shouldSimulate: true },
new Date(fakeClockMs),
);
expect.fail();
} catch (e) {
expect(e.message).to.contain('Job failed validation');
expect(expiredJob).to.deep.equal({ ..._job, status: RfqmJobStatus.FailedExpired });
}
});
it('handles an eth_call failure for meta-transaction v1 job', async () => {
const jobId = 'jobId';
const job = createMetaTransactionJobEntity(
{
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
fee: {
amount: new BigNumber(0),
token: '',
type: 'fixed',
},
inputToken: '0xinputToken',
inputTokenAmount: new BigNumber(10),
integratorId: '0xintegrator',
metaTransaction: MOCK_META_TRANSACTION,
metaTransactionHash: MOCK_META_TRANSACTION.getHash(),
minOutputTokenAmount: new BigNumber(10),
outputToken: '0xoutputToken',
takerAddress: '0xtakerAddress',
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
status: RfqmJobStatus.PendingEnqueued,
workerAddress: '0xworkeraddress',
},
jobId,
);
const _job = _.cloneDeep(job);
const gasStationAttendantMock = mock(GasStationAttendantEthereum);
when(gasStationAttendantMock.getExpectedTransactionGasRateAsync()).thenResolve(
new BigNumber(10).shiftedBy(GWEI_DECIMALS),
);
const mockDbUtils = mock(RfqmDbUtils);
when(mockDbUtils.findMetaTransactionSubmissionsByJobIdAsync(jobId)).thenResolve([]);
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.getMinOfBalancesAndAllowancesAsync(anything())).thenResolve([
new BigNumber(1000000000),
]);
when(mockBlockchainUtils.estimateGasForAsync(anything())).thenReject(new Error('fake eth call failure'));
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
rfqBlockchainUtils: instance(mockBlockchainUtils),
gasStationAttendant: instance(gasStationAttendantMock),
});
try {
await rfqmService.prepareMetaTransactionTradeAsync(
job,
'0xworkeraddress',
{ shouldValidateJob: true, shouldSimulate: true },
new Date(fakeClockMs),
);
expect.fail();
} catch (e) {
expect(e.message).to.contain('Eth call validation failed');
expect(job).to.deep.equal({
..._job,
status: RfqmJobStatus.FailedEthCallFailed,
});
}
});
it('skips the eth_call for jobs with existing submissions for meta-transaction v1 job', async () => {
const jobId = 'jobId';
const job = createMetaTransactionJobEntity(
{
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
fee: {
amount: new BigNumber(0),
token: '',
type: 'fixed',
},
inputToken: '0xinputToken',
inputTokenAmount: new BigNumber(10),
integratorId: '0xintegrator',
metaTransaction: MOCK_META_TRANSACTION,
metaTransactionHash: MOCK_META_TRANSACTION.getHash(),
minOutputTokenAmount: new BigNumber(10),
outputToken: '0xoutputToken',
takerAddress: '0xtakerAddress',
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
status: RfqmJobStatus.PendingProcessing,
workerAddress: '0xworkeraddress',
},
jobId,
);
const transaction = createMetaTransactionSubmissionEntity(
{
from: '0xworkeraddress',
metaTransactionJobId: jobId,
maxFeePerGas: new BigNumber(100000),
maxPriorityFeePerGas: new BigNumber(100),
nonce: 0,
to: '0xexchangeproxyaddress',
transactionHash: '0xsignedtransactionhash',
type: RfqmTransactionSubmissionType.Trade,
},
'submissionId',
);
const _job = _.cloneDeep(job);
const gasStationAttendantMock = mock(GasStationAttendantEthereum);
when(gasStationAttendantMock.getExpectedTransactionGasRateAsync()).thenResolve(
new BigNumber(10).shiftedBy(GWEI_DECIMALS),
);
const mockDbUtils = mock(RfqmDbUtils);
when(mockDbUtils.findMetaTransactionSubmissionsByJobIdAsync(jobId)).thenResolve([transaction]);
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(
mockBlockchainUtils.generateMetaTransactionCallData(anything(), 'v1', anything(), anything()),
).thenReturn('0xvalidcalldata');
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
rfqBlockchainUtils: instance(mockBlockchainUtils),
gasStationAttendant: instance(gasStationAttendantMock),
});
const calldata = await rfqmService.prepareMetaTransactionTradeAsync(
job,
'0xworkeraddress',
{ shouldValidateJob: true, shouldSimulate: true },
new Date(fakeClockMs),
);
expect(job).to.deep.equal(_job);
expect(calldata).to.equal('0xvalidcalldata');
verify(mockBlockchainUtils.estimateGasForAsync(anything())).never();
});
it('successfully prepares a job if `shouldValidateJob` is true for meta-transaction v1 job', async () => {
const jobId = 'jobId';
const job = createMetaTransactionJobEntity(
{
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
fee: {
amount: new BigNumber(0),
token: '',
type: 'fixed',
},
inputToken: '0xinputToken',
inputTokenAmount: new BigNumber(10),
integratorId: '0xintegrator',
metaTransaction: MOCK_META_TRANSACTION,
metaTransactionHash: MOCK_META_TRANSACTION.getHash(),
minOutputTokenAmount: new BigNumber(10),
outputToken: '0xoutputToken',
takerAddress: '0xtakerAddress',
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
status: RfqmJobStatus.PendingEnqueued,
workerAddress: '0xworkeraddress',
},
jobId,
);
const _job = _.cloneDeep(job);
const gasStationAttendantMock = mock(GasStationAttendantEthereum);
when(gasStationAttendantMock.getExpectedTransactionGasRateAsync()).thenResolve(
new BigNumber(10).shiftedBy(GWEI_DECIMALS),
);
const mockDbUtils = mock(RfqmDbUtils);
const updateRfqmJobCalledArgs: MetaTransactionJobEntity[] = [];
when(mockDbUtils.findMetaTransactionSubmissionsByJobIdAsync(jobId)).thenResolve([]);
when(mockDbUtils.updateRfqmJobAsync(anything())).thenCall(async (jobArg) => {
updateRfqmJobCalledArgs.push(_.cloneDeep(jobArg));
});
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.estimateGasForAsync(anything())).thenResolve(0);
when(
mockBlockchainUtils.generateMetaTransactionCallData(anything(), 'v1', anything(), anything()),
).thenReturn('0xvalidcalldata');
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
rfqBlockchainUtils: instance(mockBlockchainUtils),
gasStationAttendant: instance(gasStationAttendantMock),
});
const calldata = await rfqmService.prepareMetaTransactionTradeAsync(
job,
'0xworkeraddress',
{ shouldValidateJob: true, shouldSimulate: true },
new Date(fakeClockMs),
);
expect(job).to.deep.equal({
..._job,
status: RfqmJobStatus.PendingProcessing,
});
expect(calldata).to.equal('0xvalidcalldata');
expect(updateRfqmJobCalledArgs[0]).to.deep.equal({
..._job,
status: RfqmJobStatus.PendingProcessing,
});
});
it('updates the job and throws upon validation failure if `shouldValidateJob` is true for meta-transaction v2 job', async () => {
const jobId = 'jobId';
const expiredJob = createMetaTransactionV2JobEntity(
{
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeOneMinuteAgoS),
inputToken: '0xinputToken',
inputTokenAmount: new BigNumber(10),
integratorId: '0xintegrator',
calledFunction: 'transformERC20',
metaTransaction: MOCK_META_TRANSACTION_V2,
metaTransactionHash: MOCK_META_TRANSACTION_V2.getHash(),
minOutputTokenAmount: new BigNumber(10),
outputToken: '0xoutputToken',
takerAddress: '0xtakerAddress',
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
tokens: ['0xinputToken', '0xoutputToken'],
status: RfqmJobStatus.PendingEnqueued,
workerAddress: '0xworkeraddress',
},
jobId,
);
const _job = _.cloneDeep(expiredJob);
const gasStationAttendantMock = mock(GasStationAttendantEthereum);
when(gasStationAttendantMock.getExpectedTransactionGasRateAsync()).thenResolve(
new BigNumber(10).shiftedBy(GWEI_DECIMALS),
);
const mockDbUtils = mock(RfqmDbUtils);
when(mockDbUtils.findMetaTransactionV2SubmissionsByJobIdAsync(jobId)).thenResolve([]);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
gasStationAttendant: instance(gasStationAttendantMock),
});
try {
await rfqmService.prepareMetaTransactionTradeAsync(
expiredJob,
'0xworkeraddress',
{ shouldValidateJob: true, shouldSimulate: true },
new Date(fakeClockMs),
);
expect.fail();
} catch (e) {
expect(e.message).to.contain('Job failed validation');
expect(expiredJob).to.deep.equal({ ..._job, status: RfqmJobStatus.FailedExpired });
}
});
it('handles an eth_call failure for meta-transaction v2 job', async () => {
const jobId = 'jobId';
const job = createMetaTransactionV2JobEntity(
{
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
inputToken: '0xinputToken',
inputTokenAmount: new BigNumber(10),
integratorId: '0xintegrator',
calledFunction: 'transformERC20',
metaTransaction: MOCK_META_TRANSACTION_V2,
metaTransactionHash: MOCK_META_TRANSACTION_V2.getHash(),
minOutputTokenAmount: new BigNumber(10),
outputToken: '0xoutputToken',
takerAddress: '0xtakerAddress',
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
tokens: ['0xinputToken', '0xoutputToken'],
status: RfqmJobStatus.PendingEnqueued,
workerAddress: '0xworkeraddress',
},
jobId,
);
const _job = _.cloneDeep(job);
const gasStationAttendantMock = mock(GasStationAttendantEthereum);
when(gasStationAttendantMock.getExpectedTransactionGasRateAsync()).thenResolve(
new BigNumber(10).shiftedBy(GWEI_DECIMALS),
);
const mockDbUtils = mock(RfqmDbUtils);
when(mockDbUtils.findMetaTransactionV2SubmissionsByJobIdAsync(jobId)).thenResolve([]);
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.getMinOfBalancesAndAllowancesAsync(anything())).thenResolve([
new BigNumber(1000000000),
]);
when(mockBlockchainUtils.estimateGasForAsync(anything())).thenReject(new Error('fake eth call failure'));
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
rfqBlockchainUtils: instance(mockBlockchainUtils),
gasStationAttendant: instance(gasStationAttendantMock),
});
try {
await rfqmService.prepareMetaTransactionTradeAsync(
job,
'0xworkeraddress',
{ shouldValidateJob: true, shouldSimulate: true },
new Date(fakeClockMs),
);
expect.fail();
} catch (e) {
expect(e.message).to.contain('Eth call validation failed');
expect(job).to.deep.equal({
..._job,
status: RfqmJobStatus.FailedEthCallFailed,
});
}
});
it('skips the eth_call for jobs with existing submissions for meta-transaction v2 job', async () => {
const jobId = 'jobId';
const job = createMetaTransactionV2JobEntity(
{
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
inputToken: '0xinputToken',
inputTokenAmount: new BigNumber(10),
integratorId: '0xintegrator',
calledFunction: 'transformERC20',
metaTransaction: MOCK_META_TRANSACTION_V2,
metaTransactionHash: MOCK_META_TRANSACTION_V2.getHash(),
minOutputTokenAmount: new BigNumber(10),
outputToken: '0xoutputToken',
takerAddress: '0xtakerAddress',
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
tokens: ['0xinputToken', '0xoutputToken'],
status: RfqmJobStatus.PendingProcessing,
workerAddress: '0xworkeraddress',
},
jobId,
);
const transaction = createMetaTransactionV2SubmissionEntity(
{
from: '0xworkeraddress',
metaTransactionV2JobId: jobId,
maxFeePerGas: new BigNumber(100000),
maxPriorityFeePerGas: new BigNumber(100),
nonce: 0,
to: '0xexchangeproxyaddress',
transactionHash: '0xsignedtransactionhash',
type: RfqmTransactionSubmissionType.Trade,
},
'submissionId',
);
const _job = _.cloneDeep(job);
const gasStationAttendantMock = mock(GasStationAttendantEthereum);
when(gasStationAttendantMock.getExpectedTransactionGasRateAsync()).thenResolve(
new BigNumber(10).shiftedBy(GWEI_DECIMALS),
);
const mockDbUtils = mock(RfqmDbUtils);
when(mockDbUtils.findMetaTransactionV2SubmissionsByJobIdAsync(jobId)).thenResolve([transaction]);
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(
mockBlockchainUtils.generateMetaTransactionCallData(anything(), 'v2', anything(), anything()),
).thenReturn('0xvalidcalldata');
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
rfqBlockchainUtils: instance(mockBlockchainUtils),
gasStationAttendant: instance(gasStationAttendantMock),
});
const calldata = await rfqmService.prepareMetaTransactionTradeAsync(
job,
'0xworkeraddress',
{ shouldValidateJob: true, shouldSimulate: true },
new Date(fakeClockMs),
);
expect(job).to.deep.equal(_job);
expect(calldata).to.equal('0xvalidcalldata');
verify(mockBlockchainUtils.estimateGasForAsync(anything())).never();
});
it('successfully prepares a job if `shouldValidateJob` is true for meta-transaction v2 job', async () => {
const jobId = 'jobId';
const job = createMetaTransactionV2JobEntity(
{
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
inputToken: '0xinputToken',
inputTokenAmount: new BigNumber(10),
integratorId: '0xintegrator',
calledFunction: 'transformERC20',
metaTransaction: MOCK_META_TRANSACTION_V2,
metaTransactionHash: MOCK_META_TRANSACTION_V2.getHash(),
minOutputTokenAmount: new BigNumber(10),
outputToken: '0xoutputToken',
takerAddress: '0xtakerAddress',
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
tokens: ['0xinputToken', '0xoutputToken'],
status: RfqmJobStatus.PendingEnqueued,
workerAddress: '0xworkeraddress',
},
jobId,
);
const _job = _.cloneDeep(job);
const gasStationAttendantMock = mock(GasStationAttendantEthereum);
when(gasStationAttendantMock.getExpectedTransactionGasRateAsync()).thenResolve(
new BigNumber(10).shiftedBy(GWEI_DECIMALS),
);
const mockDbUtils = mock(RfqmDbUtils);
const updateRfqmJobCalledArgs: MetaTransactionJobEntity[] = [];
when(mockDbUtils.findMetaTransactionV2SubmissionsByJobIdAsync(jobId)).thenResolve([]);
when(mockDbUtils.updateRfqmJobAsync(anything())).thenCall(async (jobArg) => {
updateRfqmJobCalledArgs.push(_.cloneDeep(jobArg));
});
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.estimateGasForAsync(anything())).thenResolve(0);
when(
mockBlockchainUtils.generateMetaTransactionCallData(anything(), 'v2', anything(), anything()),
).thenReturn('0xvalidcalldata');
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
rfqBlockchainUtils: instance(mockBlockchainUtils),
gasStationAttendant: instance(gasStationAttendantMock),
});
const calldata = await rfqmService.prepareMetaTransactionTradeAsync(
job,
'0xworkeraddress',
{ shouldValidateJob: true, shouldSimulate: true },
new Date(fakeClockMs),
);
expect(job).to.deep.equal({
..._job,
status: RfqmJobStatus.PendingProcessing,
});
expect(calldata).to.equal('0xvalidcalldata');
expect(updateRfqmJobCalledArgs[0]).to.deep.equal({
..._job,
status: RfqmJobStatus.PendingProcessing,
});
});
});
describe('checkLastLookAsync', () => {
it('should call `getMinOfBalancesAndAllowancesAsync` when `shouldCheckAllowance` is true and throws when balance check fails', async () => {
const job = new RfqmV2JobEntity({
affiliateAddress: '',
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
fee: {
amount: '0',
token: '',
type: 'fixed',
},
integratorId: '',
lastLookResult: null,
makerUri: 'http://foo.bar',
makerSignature: null,
order: {
order: {
chainId: '1',
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
new BigNumber(fakeFiveMinutesLater.toString()),
new BigNumber(1),
new BigNumber(1),
).toString(),
maker: '0xmaker',
makerAmount: '1000000',
makerToken: '0xmakertoken',
taker: '0xtaker',
takerAmount: '10000000',
takerToken: '0xtakertoken',
txOrigin: '',
verifyingContract: '',
},
type: RfqmOrderTypes.Otc,
},
orderHash: '0x01234567',
status: RfqmJobStatus.PendingProcessing,
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
updatedAt: new Date(),
workerAddress: '',
workflow: 'rfqm',
});
const _job = _.cloneDeep(job);
const mockDbUtils = mock(RfqmDbUtils);
const updateRfqmJobCalledArgs: RfqmJobEntity[] = [];
when(mockDbUtils.updateRfqmJobAsync(anything())).thenCall(async (jobArg) => {
updateRfqmJobCalledArgs.push(_.cloneDeep(jobArg));
});
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.getMinOfBalancesAndAllowancesAsync(anything())).thenResolve([new BigNumber(100)]);
const mockRfqMakerBalanceCacheService = mock(RfqMakerBalanceCacheService);
when(mockRfqMakerBalanceCacheService.getERC20OwnerBalancesAsync(anything(), anything())).thenResolve([
new BigNumber(5),
]);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
rfqBlockchainUtils: instance(mockBlockchainUtils),
rfqMakerBalanceCacheService: instance(mockRfqMakerBalanceCacheService),
});
try {
await rfqmService.checkLastLookAsync(job, '0xworkeraddress', true);
expect.fail();
} catch (e) {
expect(e.message).to.contain('Order failed pre-sign validation');
expect(updateRfqmJobCalledArgs[0]).to.deep.equal({
..._job,
status: RfqmJobStatus.FailedPresignValidationFailed,
});
expect(job).to.deep.equal({
..._job,
status: RfqmJobStatus.FailedPresignValidationFailed,
});
verify(mockBlockchainUtils.getMinOfBalancesAndAllowancesAsync(anything())).once();
verify(mockBlockchainUtils.getTokenBalancesAsync(anything())).never();
}
});
it('should call `getTokenBalancesAsync` when `shouldCheckAllowance` is false and throws when balance check fails', async () => {
const job = new RfqmV2JobEntity({
affiliateAddress: '',
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
fee: {
amount: '0',
token: '',
type: 'fixed',
},
integratorId: '',
lastLookResult: null,
makerUri: 'http://foo.bar',
makerSignature: null,
order: {
order: {
chainId: '1',
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
new BigNumber(fakeFiveMinutesLater.toString()),
new BigNumber(1),
new BigNumber(1),
).toString(),
maker: '0xmaker',
makerAmount: '1000000',
makerToken: '0xmakertoken',
taker: '0xtaker',
takerAmount: '10000000',
takerToken: '0xtakertoken',
txOrigin: '',
verifyingContract: '',
},
type: RfqmOrderTypes.Otc,
},
orderHash: '0x01234567',
status: RfqmJobStatus.PendingProcessing,
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
updatedAt: new Date(),
workerAddress: '',
workflow: 'rfqm',
});
const _job = _.cloneDeep(job);
const mockDbUtils = mock(RfqmDbUtils);
const updateRfqmJobCalledArgs: RfqmJobEntity[] = [];
when(mockDbUtils.updateRfqmJobAsync(anything())).thenCall(async (jobArg) => {
updateRfqmJobCalledArgs.push(_.cloneDeep(jobArg));
});
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.getTokenBalancesAsync(anything())).thenResolve([new BigNumber(100)]);
const mockRfqMakerBalanceCacheService = mock(RfqMakerBalanceCacheService);
when(mockRfqMakerBalanceCacheService.getERC20OwnerBalancesAsync(anything(), anything())).thenResolve([
new BigNumber(5),
]);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
rfqBlockchainUtils: instance(mockBlockchainUtils),
rfqMakerBalanceCacheService: instance(mockRfqMakerBalanceCacheService),
});
try {
await rfqmService.checkLastLookAsync(job, '0xworkeraddress', false);
expect.fail();
} catch (e) {
expect(e.message).to.contain('Order failed pre-sign validation');
expect(updateRfqmJobCalledArgs[0]).to.deep.equal({
..._job,
status: RfqmJobStatus.FailedPresignValidationFailed,
});
expect(job).to.deep.equal({
..._job,
status: RfqmJobStatus.FailedPresignValidationFailed,
});
verify(mockBlockchainUtils.getTokenBalancesAsync(anything())).once();
verify(mockBlockchainUtils.getMinOfBalancesAndAllowancesAsync(anything())).never();
}
});
it('should throw when taker signature is not present', async () => {
const job = new RfqmV2JobEntity({
affiliateAddress: '',
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
fee: {
amount: '0',
token: '',
type: 'fixed',
},
integratorId: '',
lastLookResult: null,
makerUri: 'http://foo.bar',
makerSignature: null,
order: {
order: {
chainId: '1',
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
new BigNumber(fakeFiveMinutesLater.toString()),
new BigNumber(1),
new BigNumber(1),
).toString(),
maker: '0xmaker',
makerAmount: '1000000',
makerToken: '0xmakertoken',
taker: '0xtaker',
takerAmount: '10000000',
takerToken: '0xtakertoken',
txOrigin: '',
verifyingContract: '',
},
type: RfqmOrderTypes.Otc,
},
orderHash: '0x01234567',
status: RfqmJobStatus.PendingProcessing,
takerSignature: null,
updatedAt: new Date(),
workerAddress: '',
workflow: 'rfqm',
});
const _job = _.cloneDeep(job);
const mockDbUtils = mock(RfqmDbUtils);
const updateRfqmJobCalledArgs: RfqmJobEntity[] = [];
when(mockDbUtils.updateRfqmJobAsync(anything())).thenCall(async (jobArg) => {
updateRfqmJobCalledArgs.push(_.cloneDeep(jobArg));
});
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.getMinOfBalancesAndAllowancesAsync(anything())).thenResolve([
new BigNumber(1000000000),
]);
const mockRfqMakerBalanceCacheService = mock(RfqMakerBalanceCacheService);
when(mockRfqMakerBalanceCacheService.getERC20OwnerBalancesAsync(anything(), anything())).thenResolve([
new BigNumber(1000000000),
]);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
rfqBlockchainUtils: instance(mockBlockchainUtils),
rfqMakerBalanceCacheService: instance(mockRfqMakerBalanceCacheService),
});
try {
await rfqmService.checkLastLookAsync(job, '0xworkeraddress', true);
expect.fail();
} catch (e) {
expect(e.message).to.contain('Order failed pre-sign validation due to empty takerSignature');
expect(updateRfqmJobCalledArgs[0]).to.deep.equal({
..._job,
status: RfqmJobStatus.FailedPresignValidationFailed,
});
expect(job).to.deep.equal({
..._job,
status: RfqmJobStatus.FailedPresignValidationFailed,
});
}
});
it('handles decline to sign', async () => {
const job = new RfqmV2JobEntity({
affiliateAddress: '',
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
fee: {
amount: '0',
token: '',
type: 'fixed',
},
integratorId: '',
lastLookResult: null,
makerUri: 'http://foo.bar',
makerSignature: null,
order: {
order: {
chainId: '1',
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
new BigNumber(fakeFiveMinutesLater.toString()),
new BigNumber(1),
new BigNumber(1),
).toString(),
maker: '0xmaker',
makerAmount: '1000000',
makerToken: '0xmakertoken',
taker: '0xtaker',
takerAmount: '10000000',
takerToken: '0xtakertoken',
txOrigin: '',
verifyingContract: '',
},
type: RfqmOrderTypes.Otc,
},
orderHash: '0x01234567',
status: RfqmJobStatus.PendingEnqueued,
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
updatedAt: new Date(),
workerAddress: '',
workflow: 'rfqm',
});
const _job = _.cloneDeep(job);
const mockDbUtils = mock(RfqmDbUtils);
when(mockDbUtils.findV2TransactionSubmissionsByOrderHashAsync('0x01234567')).thenResolve([]);
when(mockDbUtils.findV2QuoteByOrderHashAsync('0x01234567')).thenResolve(
new RfqmV2QuoteEntity({
createdAt: new Date(),
chainId: job.chainId,
fee: job.fee,
makerUri: job.makerUri,
order: job.order,
orderHash: job.orderHash,
workflow: 'rfqm',
}),
);
const updateRfqmJobCalledArgs: RfqmJobEntity[] = [];
when(mockDbUtils.updateRfqmJobAsync(anything())).thenCall(async (jobArg) => {
updateRfqmJobCalledArgs.push(_.cloneDeep(jobArg));
});
const mockQuoteServerClient = mock(QuoteServerClient);
when(mockQuoteServerClient.signV2Async(anything(), anything(), anything())).thenResolve(undefined);
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.getMinOfBalancesAndAllowancesAsync(anything())).thenResolve([
new BigNumber(1000000000),
]);
const mockRfqMakerBalanceCacheService = mock(RfqMakerBalanceCacheService);
when(mockRfqMakerBalanceCacheService.getERC20OwnerBalancesAsync(anything(), anything())).thenResolve([
new BigNumber(1000000000),
]);
const mockCacheClient = mock(CacheClient);
const mockRfqMakerManager = mock(RfqMakerManager);
when(mockRfqMakerManager.findMakerIdWithRfqmUri(job.makerUri)).thenReturn('makerId1');
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
quoteServerClient: instance(mockQuoteServerClient),
rfqBlockchainUtils: instance(mockBlockchainUtils),
rfqMakerBalanceCacheService: instance(mockRfqMakerBalanceCacheService),
rfqMakerManager: instance(mockRfqMakerManager),
cacheClient: instance(mockCacheClient),
});
try {
await rfqmService.checkLastLookAsync(job, '0xworkeraddress', true);
expect.fail();
} catch (e) {
expect(e.message).to.contain('Market Maker declined to sign');
expect(job).to.deep.equal({
..._job,
lastLookResult: false,
status: RfqmJobStatus.FailedLastLookDeclined,
});
verify(
mockCacheClient.addMakerToCooldownAsync(
'makerId1',
anything(),
job.chainId,
job.order.order.makerToken,
job.order.order.takerToken,
),
).once();
verify(
mockDbUtils.writeV2LastLookRejectionCooldownAsync(
'makerId1',
job.chainId,
job.order.order.makerToken,
job.order.order.takerToken,
anything(),
anything(),
job.orderHash,
),
).once();
}
});
it('handles a signature failure', async () => {
const job = new RfqmV2JobEntity({
affiliateAddress: '',
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
fee: {
amount: '0',
token: '',
type: 'fixed',
},
integratorId: '',
lastLookResult: null,
makerUri: 'http://foo.bar',
makerSignature: null,
order: {
order: {
chainId: '1',
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
new BigNumber(fakeFiveMinutesLater.toString()),
new BigNumber(1),
new BigNumber(1),
).toString(),
maker: '0xmaker',
makerAmount: '1000000',
makerToken: '0xmakertoken',
taker: '0xtaker',
takerAmount: '10000000',
takerToken: '0xtakertoken',
txOrigin: '',
verifyingContract: '',
},
type: RfqmOrderTypes.Otc,
},
orderHash: '0x01234567',
status: RfqmJobStatus.PendingEnqueued,
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
updatedAt: new Date(),
workerAddress: '',
workflow: 'rfqm',
});
const _job = _.cloneDeep(job);
const mockDbUtils = mock(RfqmDbUtils);
when(mockDbUtils.findV2TransactionSubmissionsByOrderHashAsync('0x01234567')).thenResolve([]);
const mockQuoteServerClient = mock(QuoteServerClient);
when(mockQuoteServerClient.signV2Async(anything(), anything(), anything())).thenReject(
new Error('fake timeout'),
);
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.getMinOfBalancesAndAllowancesAsync(anything())).thenResolve([
new BigNumber(1000000000),
]);
const mockRfqMakerBalanceCacheService = mock(RfqMakerBalanceCacheService);
when(mockRfqMakerBalanceCacheService.getERC20OwnerBalancesAsync(anything(), anything())).thenResolve([
new BigNumber(1000000000),
]);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
quoteServerClient: instance(mockQuoteServerClient),
rfqBlockchainUtils: instance(mockBlockchainUtils),
rfqMakerBalanceCacheService: instance(mockRfqMakerBalanceCacheService),
});
try {
await rfqmService.checkLastLookAsync(job, '0xworkeraddress', true);
expect.fail();
} catch (e) {
expect(e.message).to.contain('Job failed during market maker sign attempt');
expect(job).to.deep.equal({
..._job,
status: RfqmJobStatus.FailedSignFailed,
});
}
});
it('handles signer is not the maker', async () => {
const job = new RfqmV2JobEntity({
affiliateAddress: '',
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
fee: {
amount: '0',
token: '',
type: 'fixed',
},
integratorId: '',
lastLookResult: null,
makerUri: 'http://foo.bar',
makerSignature: null,
order: {
order: {
chainId: '1',
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
new BigNumber(fakeFiveMinutesLater.toString()),
new BigNumber(1),
new BigNumber(1),
).toString(),
maker,
makerAmount: '1000000',
makerToken: '0xmakertoken',
taker: '0xtaker',
takerAmount: '10000000',
takerToken: '0xtakertoken',
txOrigin: '',
verifyingContract: '',
},
type: RfqmOrderTypes.Otc,
},
orderHash,
status: RfqmJobStatus.PendingEnqueued,
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
updatedAt: new Date(),
workerAddress: '',
workflow: 'rfqm',
});
const mockDbUtils = mock(RfqmDbUtils);
const updateRfqmJobCalledArgs: RfqmJobEntity[] = [];
when(mockDbUtils.updateRfqmJobAsync(anything())).thenCall(async (jobArg) => {
updateRfqmJobCalledArgs.push(_.cloneDeep(jobArg));
});
when(mockDbUtils.findV2TransactionSubmissionsByOrderHashAsync(orderHash)).thenResolve([]);
const mockQuoteServerClient = mock(QuoteServerClient);
const invalidEIP712Sig = _.cloneDeep(validEIP712Sig);
invalidEIP712Sig.r = '0xdc158f7b53b940863bc7b001552a90282e51033f29b73d44a2701bd16faa19d3';
when(mockQuoteServerClient.signV2Async(anything(), anything(), anything())).thenResolve(invalidEIP712Sig);
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.getMinOfBalancesAndAllowancesAsync(anything())).thenResolve([
new BigNumber(1000000000),
]);
const mockRfqMakerBalanceCacheService = mock(RfqMakerBalanceCacheService);
when(mockRfqMakerBalanceCacheService.getERC20OwnerBalancesAsync(anything(), anything())).thenResolve([
new BigNumber(1000000000),
]);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
quoteServerClient: instance(mockQuoteServerClient),
rfqBlockchainUtils: instance(mockBlockchainUtils),
rfqMakerBalanceCacheService: instance(mockRfqMakerBalanceCacheService),
});
try {
await rfqmService.checkLastLookAsync(job, '0xworkeraddress', true);
expect.fail();
} catch (e) {
expect(e.message).to.contain('Invalid order signer address');
expect(job.status).to.deep.equal(RfqmJobStatus.FailedSignFailed);
}
});
it('updates market maker signatures missing bytes', async () => {
const job = new RfqmV2JobEntity({
affiliateAddress: '',
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
fee: {
amount: '0',
token: '',
type: 'fixed',
},
integratorId: '',
lastLookResult: null,
makerUri: 'http://foo.bar',
makerSignature: null,
order: {
order: {
chainId: '1',
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
new BigNumber(fakeFiveMinutesLater.toString()),
new BigNumber(1),
new BigNumber(1),
).toString(),
maker,
makerAmount: '1000000',
makerToken: '0xmakertoken',
taker: '0xtaker',
takerAmount: '10000000',
takerToken: '0xtakertoken',
txOrigin: '',
verifyingContract: '',
},
type: RfqmOrderTypes.Otc,
},
orderHash,
status: RfqmJobStatus.PendingEnqueued,
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
updatedAt: new Date(),
workerAddress: '',
workflow: 'rfqm',
});
const _job = _.cloneDeep(job);
const mockDbUtils = mock(RfqmDbUtils);
when(mockDbUtils.findV2TransactionSubmissionsByOrderHashAsync(anything())).thenResolve([]);
const mockQuoteServerClient = mock(QuoteServerClient);
when(mockQuoteServerClient.signV2Async(anything(), anything(), anything())).thenResolve(missingByteSig);
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.isValidOrderSignerAsync(anything(), anything())).thenResolve(true);
when(mockBlockchainUtils.getMinOfBalancesAndAllowancesAsync(anything())).thenResolve([
new BigNumber(1000000000),
]);
when(mockBlockchainUtils.estimateGasForAsync(anything())).thenResolve(0);
when(
mockBlockchainUtils.generateTakerSignedOtcOrderCallData(
anything(),
anything(),
anything(),
anything(),
anything(),
),
).thenReturn('0xvalidcalldata');
const mockRfqMakerBalanceCacheService = mock(RfqMakerBalanceCacheService);
when(mockRfqMakerBalanceCacheService.getERC20OwnerBalancesAsync(anything(), anything())).thenResolve([
new BigNumber(1000000000),
]);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
quoteServerClient: instance(mockQuoteServerClient),
rfqBlockchainUtils: instance(mockBlockchainUtils),
rfqMakerBalanceCacheService: instance(mockRfqMakerBalanceCacheService),
});
await rfqmService.checkLastLookAsync(job, '0xworkeraddress', true);
expect(job).to.deep.equal({
..._job,
lastLookResult: true,
makerSignature: padSignature(missingByteSig),
status: RfqmJobStatus.PendingLastLookAccepted,
});
});
it('does not sign if maker signature is already present', async () => {
const job = new RfqmV2JobEntity({
affiliateAddress: '',
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(fakeFiveMinutesLater),
fee: {
amount: '0',
token: '',
type: 'fixed',
},
integratorId: '',
lastLookResult: true,
makerUri: 'http://foo.bar',
makerSignature: validEIP712Sig,
order: {
order: {
chainId: '1',
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
new BigNumber(fakeFiveMinutesLater.toString()),
new BigNumber(1),
new BigNumber(1),
).toString(),
maker,
makerAmount: '1000000',
makerToken: '0xmakertoken',
taker: '0xtaker',
takerAmount: '10000000',
takerToken: '0xtakertoken',
txOrigin: '',
verifyingContract: '',
},
type: RfqmOrderTypes.Otc,
},
orderHash,
status: RfqmJobStatus.PendingLastLookAccepted,
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
updatedAt: new Date(),
workerAddress: '',
workflow: 'rfqm',
});
const _job = _.cloneDeep(job);
const mockDbUtils = mock(RfqmDbUtils);
const mockQuoteServerClient = mock(QuoteServerClient);
const mockBlockchainUtils = mock(RfqBlockchainUtils);
const mockRfqMakerBalanceCacheService = mock(RfqMakerBalanceCacheService);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
quoteServerClient: instance(mockQuoteServerClient),
rfqBlockchainUtils: instance(mockBlockchainUtils),
rfqMakerBalanceCacheService: instance(mockRfqMakerBalanceCacheService),
});
await rfqmService.checkLastLookAsync(job, '0xworkeraddress', true);
expect(job).to.deep.equal({
..._job,
lastLookResult: true,
makerSignature: validEIP712Sig,
status: RfqmJobStatus.PendingLastLookAccepted,
});
});
});
describe('submitToChainAsync', () => {
describe('kind is `rfqm_v2_job`', () => {
it('submits a transaction successfully when there is no previous transaction', async () => {
const nowS = Math.round(new Date().getTime() / ONE_SECOND_MS);
const job = new RfqmV2JobEntity({
affiliateAddress: '',
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(nowS + 600),
fee: {
amount: '0',
token: '',
type: 'fixed',
},
integratorId: '',
lastLookResult: true,
makerUri: 'http://foo.bar',
order: {
order: {
chainId: '1',
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
new BigNumber(nowS + 600),
new BigNumber(1),
new BigNumber(1),
).toString(),
maker: '0xmaker',
makerAmount: '1000000',
makerToken: '0xmakertoken',
taker: '0xtaker',
takerAmount: '10000000',
takerToken: '0xtakertoken',
txOrigin: '',
verifyingContract: '',
},
type: RfqmOrderTypes.Otc,
},
orderHash: '0x01234567',
status: RfqmJobStatus.PendingLastLookAccepted,
updatedAt: new Date(),
workerAddress: '',
workflow: 'rfqm',
});
const mockTransactionRequest: providers.TransactionRequest = {};
const mockTransaction = new RfqmV2TransactionSubmissionEntity({
from: '0xworkeraddress',
maxFeePerGas: new BigNumber(100000),
maxPriorityFeePerGas: new BigNumber(100),
nonce: 0,
orderHash: '0x01234567',
to: '0xexchangeproxyaddress',
transactionHash: '0xsignedtransactionhash',
type: RfqmTransactionSubmissionType.Trade,
});
const mockTransactionReceipt: providers.TransactionReceipt = {
to: '0xto',
from: '0xfrom',
contractAddress: '0xexchangeproxyaddress',
transactionIndex: 0,
gasUsed: EthersBigNumber.from(10000),
logsBloom: '',
blockHash: '0xblockhash',
transactionHash: '0xsignedtransactionhash',
logs: [],
blockNumber: 1,
confirmations: 3,
cumulativeGasUsed: EthersBigNumber.from(1000),
effectiveGasPrice: EthersBigNumber.from(1000),
byzantium: true,
type: 2,
status: 1,
};
const mockNonce = 0;
const gasStationAttendantMock = mock(GasStationAttendantEthereum);
when(gasStationAttendantMock.getExpectedTransactionGasRateAsync()).thenResolve(
new BigNumber(10).shiftedBy(GWEI_DECIMALS),
);
const mockDbUtils = mock(RfqmDbUtils);
when(
mockDbUtils.findV2TransactionSubmissionsByOrderHashAsync(
'0x01234567',
deepEqual([RfqmTransactionSubmissionType.Trade]),
),
).thenResolve([]);
const updateRfqmJobCalledArgs: RfqmJobEntity[] = [];
when(mockDbUtils.updateRfqmJobAsync(anything())).thenCall(async (jobArg) => {
updateRfqmJobCalledArgs.push(_.cloneDeep(jobArg));
});
const writeV2RfqmTransactionSubmissionToDbCalledArgs: RfqmTransactionSubmissionEntity[] = [];
when(mockDbUtils.writeV2RfqmTransactionSubmissionToDbAsync(anything())).thenCall(
async (transactionArg) => {
writeV2RfqmTransactionSubmissionToDbCalledArgs.push(_.cloneDeep(transactionArg));
return _.cloneDeep(mockTransaction);
},
);
when(
mockDbUtils.findV2TransactionSubmissionByTransactionHashAsync('0xsignedtransactionhash'),
).thenResolve(_.cloneDeep(mockTransaction));
const updateRfqmTransactionSubmissionsCalledArgs: RfqmTransactionSubmissionEntity[][] = [];
when(mockDbUtils.updateRfqmTransactionSubmissionsAsync(anything())).thenCall(async (tranactionArg) => {
updateRfqmTransactionSubmissionsCalledArgs.push(_.cloneDeep(tranactionArg));
});
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.getNonceAsync('0xworkeraddress')).thenResolve(mockNonce);
when(mockBlockchainUtils.estimateGasForAsync(anything())).thenResolve(100);
when(
mockBlockchainUtils.transformTxDataToTransactionRequest(anything(), anything(), anything()),
).thenReturn(mockTransactionRequest);
when(mockBlockchainUtils.signTransactionAsync(anything())).thenResolve({
signedTransaction: 'signedTransaction',
transactionHash: '0xsignedtransactionhash',
});
when(mockBlockchainUtils.getExchangeProxyAddress()).thenReturn('0xexchangeproxyaddress');
when(mockBlockchainUtils.submitSignedTransactionAsync(anything())).thenResolve(
'0xsignedtransactionhash',
);
when(mockBlockchainUtils.getReceiptsAsync(deepEqual(['0xsignedtransactionhash']))).thenResolve([
mockTransactionReceipt,
]);
when(mockBlockchainUtils.getCurrentBlockAsync()).thenResolve(4);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
gasStationAttendant: instance(gasStationAttendantMock),
rfqBlockchainUtils: instance(mockBlockchainUtils),
});
const callback = async (
newSubmissionContextStatus: SubmissionContextStatus,
oldSubmissionContextStatus?: SubmissionContextStatus,
): Promise<void> => {
if (newSubmissionContextStatus !== oldSubmissionContextStatus) {
const newJobStatus =
SubmissionContext.tradeSubmissionContextStatusToJobStatus(newSubmissionContextStatus);
job.status = newJobStatus;
await mockDbUtils.updateRfqmJobAsync(job);
}
};
await rfqmService.submitToChainAsync({
kind: job.kind,
to: '0xexchangeproxyaddress',
from: '0xworkeraddress',
calldata: '0xcalldata',
expiry: job.expiry,
identifier: job.orderHash,
submissionType: RfqmTransactionSubmissionType.Trade,
onSubmissionContextStatusUpdate: callback,
});
verify(mockBlockchainUtils.estimateGasForAsync(anything()));
// eth_createAccessList should not be called when not enabled
verify(mockBlockchainUtils.createAccessListForAsync(anything())).never();
expect(job.status).to.equal(RfqmJobStatus.SucceededConfirmed);
expect(writeV2RfqmTransactionSubmissionToDbCalledArgs[0].status).to.equal(
RfqmTransactionSubmissionStatus.Presubmit,
);
expect(updateRfqmTransactionSubmissionsCalledArgs[0][0].status).to.equal(
RfqmTransactionSubmissionStatus.Submitted,
);
expect(updateRfqmTransactionSubmissionsCalledArgs[1][0].status).to.equal(
RfqmTransactionSubmissionStatus.SucceededConfirmed,
);
});
it("ignores an existing PRESUBMIT transaction which isn't found in the mempool or on chain", async () => {
const nowS = Math.round(new Date().getTime() / ONE_SECOND_MS);
const job = new RfqmV2JobEntity({
affiliateAddress: '',
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(nowS + 600),
fee: {
amount: '0',
token: '',
type: 'fixed',
},
integratorId: '',
lastLookResult: true,
makerUri: 'http://foo.bar',
order: {
order: {
chainId: '1',
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
new BigNumber(nowS + 600),
new BigNumber(1),
new BigNumber(1),
).toString(),
maker: '0xmaker',
makerAmount: '1000000',
makerToken: '0xmakertoken',
taker: '0xtaker',
takerAmount: '10000000',
takerToken: '0xtakertoken',
txOrigin: '',
verifyingContract: '',
},
type: RfqmOrderTypes.Otc,
},
orderHash: '0x01234567',
status: RfqmJobStatus.PendingLastLookAccepted,
updatedAt: new Date(),
workerAddress: '',
workflow: 'rfqm',
});
const mockPresubmitTransaction = new RfqmV2TransactionSubmissionEntity({
createdAt: new Date(1233),
from: '0xworkeraddress',
maxFeePerGas: new BigNumber(100000),
maxPriorityFeePerGas: new BigNumber(100),
nonce: 0,
orderHash: '0x01234567',
status: RfqmTransactionSubmissionStatus.Presubmit,
to: '0xexchangeproxyaddress',
transactionHash: '0xpresubmittransactionhash',
type: RfqmTransactionSubmissionType.Trade,
});
const mockTransactionRequest: providers.TransactionRequest = {};
const mockTransaction = new RfqmV2TransactionSubmissionEntity({
from: '0xworkeraddress',
maxFeePerGas: new BigNumber(100000),
maxPriorityFeePerGas: new BigNumber(100),
nonce: 0,
orderHash: '0x01234567',
to: '0xexchangeproxyaddress',
transactionHash: '0xsignedtransactionhash',
type: RfqmTransactionSubmissionType.Trade,
});
const mockTransactionReceipt: providers.TransactionReceipt = {
to: '0xexchangeproxyaddress',
from: '0xworkeraddress',
contractAddress: '0xexchangeproxyaddress',
transactionIndex: 0,
gasUsed: EthersBigNumber.from(10000),
logsBloom: '',
blockHash: '0xblockhash',
transactionHash: '0xsignedtransactionhash',
logs: [],
blockNumber: 1,
confirmations: 3,
cumulativeGasUsed: EthersBigNumber.from(1000),
effectiveGasPrice: EthersBigNumber.from(1000),
byzantium: true,
type: 2,
status: 1,
};
const mockNonce = 0;
const gasStationAttendantMock = mock(GasStationAttendantEthereum);
when(gasStationAttendantMock.getExpectedTransactionGasRateAsync()).thenResolve(
new BigNumber(10).shiftedBy(GWEI_DECIMALS),
);
const mockDbUtils = mock(RfqmDbUtils);
when(
mockDbUtils.findV2TransactionSubmissionsByOrderHashAsync(
'0x01234567',
deepEqual([RfqmTransactionSubmissionType.Trade]),
),
).thenResolve([mockPresubmitTransaction]);
const updateRfqmJobCalledArgs: RfqmJobEntity[] = [];
when(mockDbUtils.updateRfqmJobAsync(anything())).thenCall(async (jobArg) => {
updateRfqmJobCalledArgs.push(_.cloneDeep(jobArg));
});
const writeV2RfqmTransactionSubmissionToDbCalledArgs: RfqmTransactionSubmissionEntity[] = [];
when(mockDbUtils.writeV2RfqmTransactionSubmissionToDbAsync(anything())).thenCall(
async (transactionArg) => {
writeV2RfqmTransactionSubmissionToDbCalledArgs.push(_.cloneDeep(transactionArg));
return _.cloneDeep(mockTransaction);
},
);
when(
mockDbUtils.findV2TransactionSubmissionByTransactionHashAsync('0xsignedtransactionhash'),
).thenResolve(_.cloneDeep(mockTransaction));
const updateRfqmTransactionSubmissionsCalledArgs: RfqmV2TransactionSubmissionEntity[][] = [];
when(mockDbUtils.updateRfqmTransactionSubmissionsAsync(anything())).thenCall(async (tranactionArg) => {
updateRfqmTransactionSubmissionsCalledArgs.push(_.cloneDeep(tranactionArg));
});
const mockBlockchainUtils = mock(RfqBlockchainUtils);
// This mock response indicates that the presubmit transaction can't be found
// on chain or in the mempool
when(mockBlockchainUtils.getTransactionAsync('0xpresubmittransactionhash')).thenResolve(null);
when(mockBlockchainUtils.getNonceAsync('0xworkeraddress')).thenResolve(mockNonce);
when(mockBlockchainUtils.estimateGasForAsync(anything())).thenResolve(100);
when(
mockBlockchainUtils.transformTxDataToTransactionRequest(anything(), anything(), anything()),
).thenReturn(mockTransactionRequest);
when(mockBlockchainUtils.signTransactionAsync(anything())).thenResolve({
signedTransaction: 'signedTransaction',
transactionHash: '0xsignedtransactionhash',
});
when(mockBlockchainUtils.getExchangeProxyAddress()).thenReturn('0xexchangeproxyaddress');
when(mockBlockchainUtils.submitSignedTransactionAsync(anything())).thenResolve(
'0xsignedtransactionhash',
);
when(mockBlockchainUtils.getReceiptsAsync(deepEqual(['0xsignedtransactionhash']))).thenResolve([
mockTransactionReceipt,
]);
when(mockBlockchainUtils.getCurrentBlockAsync()).thenResolve(4);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
gasStationAttendant: instance(gasStationAttendantMock),
rfqBlockchainUtils: instance(mockBlockchainUtils),
});
const callback = async (
newSubmissionContextStatus: SubmissionContextStatus,
oldSubmissionContextStatus?: SubmissionContextStatus,
): Promise<void> => {
if (newSubmissionContextStatus !== oldSubmissionContextStatus) {
const newJobStatus =
SubmissionContext.tradeSubmissionContextStatusToJobStatus(newSubmissionContextStatus);
job.status = newJobStatus;
await mockDbUtils.updateRfqmJobAsync(job);
}
};
await rfqmService.submitToChainAsync({
kind: job.kind,
to: '0xexchangeproxyaddress',
from: '0xworkeraddress',
calldata: '0xcalldata',
expiry: job.expiry,
identifier: job.orderHash,
submissionType: RfqmTransactionSubmissionType.Trade,
onSubmissionContextStatusUpdate: callback,
});
// eth_createAccessList should not be called when not enabled
verify(mockBlockchainUtils.createAccessListForAsync(anything())).never();
expect(job.status).to.equal(RfqmJobStatus.SucceededConfirmed);
// Expectations are the same as if the presubmit transaction never existed
expect(writeV2RfqmTransactionSubmissionToDbCalledArgs[0].status).to.equal(
RfqmTransactionSubmissionStatus.Presubmit,
);
expect(updateRfqmTransactionSubmissionsCalledArgs[0][0].status).to.equal(
RfqmTransactionSubmissionStatus.Submitted,
);
expect(updateRfqmTransactionSubmissionsCalledArgs[1][0].status).to.equal(
RfqmTransactionSubmissionStatus.SucceededConfirmed,
);
});
it("marks a PRESUBMIT job as expired when existing transactions aren't found in \
the mempool or on chain and the expiration time has passed", async () => {
const nowS = Math.round(new Date().getTime() / ONE_SECOND_MS);
const job = new RfqmV2JobEntity({
affiliateAddress: '',
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(nowS - 60),
fee: {
amount: '0',
token: '',
type: 'fixed',
},
integratorId: '',
lastLookResult: true,
makerUri: 'http://foo.bar',
order: {
order: {
chainId: '1',
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
new BigNumber(nowS - 60),
new BigNumber(1),
new BigNumber(1),
).toString(),
maker: '0xmaker',
makerAmount: '1000000',
makerToken: '0xmakertoken',
taker: '0xtaker',
takerAmount: '10000000',
takerToken: '0xtakertoken',
txOrigin: '',
verifyingContract: '',
},
type: RfqmOrderTypes.Otc,
},
orderHash: '0x01234567',
status: RfqmJobStatus.PendingLastLookAccepted,
updatedAt: new Date(),
workerAddress: '',
workflow: 'rfqm',
});
const mockPresubmitTransaction = new RfqmV2TransactionSubmissionEntity({
createdAt: new Date(1233),
from: '0xworkeraddress',
maxFeePerGas: new BigNumber(100000),
maxPriorityFeePerGas: new BigNumber(100),
nonce: 0,
orderHash: '0x01234567',
status: RfqmTransactionSubmissionStatus.Presubmit,
to: '0xexchangeproxyaddress',
transactionHash: '0xpresubmittransactionhash',
type: RfqmTransactionSubmissionType.Trade,
});
const mockTransactionRequest: providers.TransactionRequest = {};
const mockTransaction = new RfqmV2TransactionSubmissionEntity({
from: '0xworkeraddress',
maxFeePerGas: new BigNumber(100000),
maxPriorityFeePerGas: new BigNumber(100),
nonce: 0,
orderHash: '0x01234567',
to: '0xexchangeproxyaddress',
transactionHash: '0xsignedtransactionhash',
type: RfqmTransactionSubmissionType.Trade,
});
const mockTransactionReceipt: providers.TransactionReceipt = {
to: '0xexchangeproxyaddress',
from: '0xworkeraddress',
contractAddress: '0xexchangeproxyaddress',
transactionIndex: 0,
gasUsed: EthersBigNumber.from(10000),
logsBloom: '',
blockHash: '0xblockhash',
transactionHash: '0xsignedtransactionhash',
logs: [],
blockNumber: 1,
confirmations: 3,
cumulativeGasUsed: EthersBigNumber.from(1000),
effectiveGasPrice: EthersBigNumber.from(1000),
byzantium: true,
type: 2,
status: 1,
};
const mockNonce = 0;
const gasStationAttendantMock = mock(GasStationAttendantEthereum);
when(gasStationAttendantMock.getExpectedTransactionGasRateAsync()).thenResolve(
new BigNumber(10).shiftedBy(GWEI_DECIMALS),
);
const mockDbUtils = mock(RfqmDbUtils);
when(
mockDbUtils.findV2TransactionSubmissionsByOrderHashAsync(
'0x01234567',
deepEqual([RfqmTransactionSubmissionType.Trade]),
),
).thenResolve([mockPresubmitTransaction]);
const updateRfqmJobCalledArgs: RfqmJobEntity[] = [];
when(mockDbUtils.updateRfqmJobAsync(anything())).thenCall(async (jobArg) => {
updateRfqmJobCalledArgs.push(_.cloneDeep(jobArg));
});
const writeV2RfqmTransactionSubmissionToDbCalledArgs: RfqmTransactionSubmissionEntity[] = [];
when(mockDbUtils.writeV2RfqmTransactionSubmissionToDbAsync(anything())).thenCall(
async (transactionArg) => {
writeV2RfqmTransactionSubmissionToDbCalledArgs.push(_.cloneDeep(transactionArg));
return _.cloneDeep(mockTransaction);
},
);
when(
mockDbUtils.findV2TransactionSubmissionByTransactionHashAsync('0xsignedtransactionhash'),
).thenResolve(_.cloneDeep(mockTransaction));
const updateRfqmTransactionSubmissionsCalledArgs: RfqmV2TransactionSubmissionEntity[][] = [];
when(mockDbUtils.updateRfqmTransactionSubmissionsAsync(anything())).thenCall(async (tranactionArg) => {
updateRfqmTransactionSubmissionsCalledArgs.push(_.cloneDeep(tranactionArg));
});
const mockBlockchainUtils = mock(RfqBlockchainUtils);
// This mock response indicates that the presubmit transaction can't be found
// on chain or in the mempool
when(mockBlockchainUtils.getTransactionAsync('0xpresubmittransactionhash')).thenResolve(null);
when(mockBlockchainUtils.getNonceAsync('0xworkeraddress')).thenResolve(mockNonce);
when(mockBlockchainUtils.estimateGasForAsync(anything())).thenResolve(100);
when(
mockBlockchainUtils.transformTxDataToTransactionRequest(anything(), anything(), anything()),
).thenReturn(mockTransactionRequest);
when(mockBlockchainUtils.signTransactionAsync(anything())).thenResolve({
signedTransaction: 'signedTransaction',
transactionHash: '0xsignedtransactionhash',
});
when(mockBlockchainUtils.getExchangeProxyAddress()).thenReturn('0xexchangeproxyaddress');
when(mockBlockchainUtils.submitSignedTransactionAsync(anything())).thenResolve(
'0xsignedtransactionhash',
);
when(mockBlockchainUtils.getReceiptsAsync(deepEqual(['0xsignedtransactionhash']))).thenResolve([
mockTransactionReceipt,
]);
when(mockBlockchainUtils.getCurrentBlockAsync()).thenResolve(4);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
gasStationAttendant: instance(gasStationAttendantMock),
rfqBlockchainUtils: instance(mockBlockchainUtils),
});
const callback = async (
newSubmissionContextStatus: SubmissionContextStatus,
oldSubmissionContextStatus?: SubmissionContextStatus,
): Promise<void> => {
if (newSubmissionContextStatus !== oldSubmissionContextStatus) {
const newJobStatus =
SubmissionContext.tradeSubmissionContextStatusToJobStatus(newSubmissionContextStatus);
job.status = newJobStatus;
await mockDbUtils.updateRfqmJobAsync(job);
}
};
try {
await rfqmService.submitToChainAsync({
kind: job.kind,
to: '0xexchangeproxyaddress',
from: '0xworkeraddress',
calldata: '0xcalldata',
expiry: job.expiry,
identifier: job.orderHash,
submissionType: RfqmTransactionSubmissionType.Trade,
onSubmissionContextStatusUpdate: callback,
});
expect.fail();
} catch (e) {
expect(e.message).to.contain('Exceed expiry');
// eth_createAccessList should not be called when not enabled
verify(mockBlockchainUtils.createAccessListForAsync(anything())).never();
expect(job.status).to.equal(RfqmJobStatus.FailedExpired);
}
});
it('recovers a PRESUBMIT transaction which actually submitted', async () => {
const nowS = Math.round(new Date().getTime() / ONE_SECOND_MS);
const job = new RfqmV2JobEntity({
affiliateAddress: '',
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(nowS + 600),
fee: {
amount: '0',
token: '',
type: 'fixed',
},
integratorId: '',
lastLookResult: true,
makerUri: 'http://foo.bar',
order: {
order: {
chainId: '1',
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
new BigNumber(nowS + 600),
new BigNumber(1),
new BigNumber(1),
).toString(),
maker: '0xmaker',
makerAmount: '1000000',
makerToken: '0xmakertoken',
taker: '0xtaker',
takerAmount: '10000000',
takerToken: '0xtakertoken',
txOrigin: '',
verifyingContract: '',
},
type: RfqmOrderTypes.Otc,
},
orderHash: '0x01234567',
status: RfqmJobStatus.PendingLastLookAccepted,
updatedAt: new Date(),
workerAddress: '',
workflow: 'rfqm',
});
const mockPresubmitTransaction = new RfqmV2TransactionSubmissionEntity({
createdAt: new Date(1233),
from: '0xworkeraddress',
maxFeePerGas: new BigNumber(100000),
maxPriorityFeePerGas: new BigNumber(100),
nonce: 0,
orderHash: '0x01234567',
status: RfqmTransactionSubmissionStatus.Presubmit,
to: '0xexchangeproxyaddress',
transactionHash: '0xpresubmittransactionhash',
type: RfqmTransactionSubmissionType.Trade,
});
const mockTransactionReceipt: providers.TransactionReceipt = {
blockHash: '0xblockhash',
blockNumber: 1,
byzantium: true,
confirmations: 3,
contractAddress: '0xexchangeproxyaddress',
cumulativeGasUsed: EthersBigNumber.from(1000),
effectiveGasPrice: EthersBigNumber.from(1000),
from: '0xworkeraddress',
gasUsed: EthersBigNumber.from(10000),
logs: [],
logsBloom: '',
status: 1,
to: '0xexchangeproxyaddress',
transactionHash: '0xpresubmittransactionhash',
transactionIndex: 0,
type: 2,
};
const mockTransactionResponse: providers.TransactionResponse = {
chainId: 1,
confirmations: 0,
data: '',
from: '0xworkeraddress',
gasLimit: EthersBigNumber.from(1000000),
hash: '0xpresubmittransactionhash',
nonce: 0,
type: 2,
value: EthersBigNumber.from(0),
wait: (_confirmations: number | undefined) => Promise.resolve(mockTransactionReceipt),
};
const mockMinedBlock: providers.Block = {
_difficulty: EthersBigNumber.from(2),
difficulty: 2,
extraData: '',
gasLimit: EthersBigNumber.from(1000),
gasUsed: EthersBigNumber.from(1000),
hash: '0xblockhash',
miner: '0xminer',
nonce: '0x000',
number: 21,
parentHash: '0xparentblockhash',
timestamp: 12345,
transactions: ['0xpresubmittransactionhash'],
};
const mockNonce = 0;
const gasStationAttendantMock = mock(GasStationAttendantEthereum);
when(gasStationAttendantMock.getExpectedTransactionGasRateAsync()).thenResolve(
new BigNumber(10).shiftedBy(GWEI_DECIMALS),
);
const mockDbUtils = mock(RfqmDbUtils);
when(
mockDbUtils.findV2TransactionSubmissionsByOrderHashAsync(
'0x01234567',
deepEqual([RfqmTransactionSubmissionType.Trade]),
),
).thenResolve([mockPresubmitTransaction]);
const updateRfqmTransactionSubmissionsCalledArgs: RfqmV2TransactionSubmissionEntity[][] = [];
when(mockDbUtils.updateRfqmTransactionSubmissionsAsync(anything())).thenCall(async (tranactionArg) => {
updateRfqmTransactionSubmissionsCalledArgs.push(_.cloneDeep(tranactionArg));
});
const mockBlockchainUtils = mock(RfqBlockchainUtils);
// This mock response indicates that the transaction is present in the mempool
when(mockBlockchainUtils.getTransactionAsync('0xpresubmittransactionhash')).thenResolve(
mockTransactionResponse,
);
when(mockBlockchainUtils.getNonceAsync('0xworkeraddress')).thenResolve(mockNonce);
when(mockBlockchainUtils.estimateGasForAsync(anything())).thenReject(
new Error('estimateGasForAsync called during recovery'),
);
when(mockBlockchainUtils.getReceiptsAsync(deepEqual(['0xpresubmittransactionhash']))).thenResolve([
mockTransactionReceipt,
]);
when(mockBlockchainUtils.getBlockAsync('0xblockhash')).thenResolve(mockMinedBlock);
when(mockBlockchainUtils.getCurrentBlockAsync()).thenResolve(4);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
gasStationAttendant: instance(gasStationAttendantMock),
rfqBlockchainUtils: instance(mockBlockchainUtils),
});
const callback = async (
newSubmissionContextStatus: SubmissionContextStatus,
oldSubmissionContextStatus?: SubmissionContextStatus,
): Promise<void> => {
if (newSubmissionContextStatus !== oldSubmissionContextStatus) {
const newJobStatus =
SubmissionContext.tradeSubmissionContextStatusToJobStatus(newSubmissionContextStatus);
job.status = newJobStatus;
await mockDbUtils.updateRfqmJobAsync(job);
}
};
await rfqmService.submitToChainAsync({
kind: job.kind,
to: '0xexchangeproxyaddress',
from: '0xworkeraddress',
calldata: '0xcalldata',
expiry: job.expiry,
identifier: job.orderHash,
submissionType: RfqmTransactionSubmissionType.Trade,
onSubmissionContextStatusUpdate: callback,
});
// eth_createAccessList should not be called when not enabled
verify(mockBlockchainUtils.createAccessListForAsync(anything())).never();
// Logic should first check to see if the transaction was actually sent.
// If it was (and it is being mock so in this test) then the logic first
// updates the status of the transaction to "Submitted"
expect(updateRfqmTransactionSubmissionsCalledArgs[0][0].status).to.equal(
RfqmTransactionSubmissionStatus.Submitted,
);
// The logic then enters the watch loop. On the first check, a transaction
// receipt exists for this transaction and it will be marked "confirmed"
expect(updateRfqmTransactionSubmissionsCalledArgs[1][0].status).to.equal(
RfqmTransactionSubmissionStatus.SucceededConfirmed,
);
expect(job.status).to.equal(RfqmJobStatus.SucceededConfirmed);
});
it('finalizes a job to FAILED_EXPIRED once the expiration window has passed', async () => {
const nowS = Math.round(new Date().getTime() / ONE_SECOND_MS);
const ninetySecondsAgo = nowS - 100;
const job = new RfqmV2JobEntity({
affiliateAddress: '',
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(ninetySecondsAgo),
fee: {
amount: '0',
token: '',
type: 'fixed',
},
integratorId: '',
lastLookResult: true,
makerUri: 'http://foo.bar',
order: {
order: {
chainId: '1',
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
new BigNumber(ninetySecondsAgo.toString()),
new BigNumber(1),
new BigNumber(1),
).toString(),
maker: '0xmaker',
makerAmount: '1000000',
makerToken: '0xmakertoken',
taker: '0xtaker',
takerAmount: '10000000',
takerToken: '0xtakertoken',
txOrigin: '',
verifyingContract: '',
},
type: RfqmOrderTypes.Otc,
},
orderHash: '0x01234567',
status: RfqmJobStatus.PendingSubmitted,
updatedAt: new Date(),
workerAddress: '',
workflow: 'rfqm',
});
const mockTransaction = new RfqmV2TransactionSubmissionEntity({
createdAt: new Date(1233),
from: '0xworkeraddress',
maxFeePerGas: new BigNumber(100000),
maxPriorityFeePerGas: new BigNumber(100),
nonce: 0,
orderHash: '0x01234567',
status: RfqmTransactionSubmissionStatus.Submitted,
to: '0xexchangeproxyaddress',
transactionHash: '0xpresubmittransactionhash',
type: RfqmTransactionSubmissionType.Trade,
});
const gasStationAttendantMock = mock(GasStationAttendantEthereum);
when(gasStationAttendantMock.getExpectedTransactionGasRateAsync()).thenResolve(
new BigNumber(10).shiftedBy(GWEI_DECIMALS),
);
const mockDbUtils = mock(RfqmDbUtils);
when(
mockDbUtils.findV2TransactionSubmissionsByOrderHashAsync(
'0x01234567',
deepEqual([RfqmTransactionSubmissionType.Trade]),
),
).thenResolve([mockTransaction]);
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.estimateGasForAsync(anything())).thenResolve(100);
when(mockBlockchainUtils.getReceiptsAsync(deepEqual(['0xpresubmittransactionhash']))).thenResolve([]);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
gasStationAttendant: instance(gasStationAttendantMock),
rfqBlockchainUtils: instance(mockBlockchainUtils),
});
const callback = async (
newSubmissionContextStatus: SubmissionContextStatus,
oldSubmissionContextStatus?: SubmissionContextStatus,
): Promise<void> => {
if (newSubmissionContextStatus !== oldSubmissionContextStatus) {
const newJobStatus =
SubmissionContext.tradeSubmissionContextStatusToJobStatus(newSubmissionContextStatus);
job.status = newJobStatus;
await mockDbUtils.updateRfqmJobAsync(job);
}
};
try {
await rfqmService.submitToChainAsync({
kind: job.kind,
to: '0xexchangeproxyaddress',
from: '0xworkeraddress',
calldata: '0xcalldata',
expiry: job.expiry,
identifier: job.orderHash,
submissionType: RfqmTransactionSubmissionType.Trade,
onSubmissionContextStatusUpdate: callback,
});
} catch (e) {
expect(e.message).to.contain('Exceed expiry');
// eth_createAccessList should not be called when not enabled
verify(mockBlockchainUtils.createAccessListForAsync(anything())).never();
expect(job.status).to.equal(RfqmJobStatus.FailedExpired);
}
});
it('should call createAccessListForAsync and should not affect the overall method when RPC returns properly', async () => {
const nowS = Math.round(new Date().getTime() / ONE_SECOND_MS);
const job = new RfqmV2JobEntity({
affiliateAddress: '',
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(nowS + 600),
fee: {
amount: '0',
token: '',
type: 'fixed',
},
integratorId: '',
lastLookResult: true,
makerUri: 'http://foo.bar',
order: {
order: {
chainId: '1',
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
new BigNumber(nowS + 600),
new BigNumber(1),
new BigNumber(1),
).toString(),
maker: '0xmaker',
makerAmount: '1000000',
makerToken: '0xmakertoken',
taker: '0xtaker',
takerAmount: '10000000',
takerToken: '0xtakertoken',
txOrigin: '',
verifyingContract: '',
},
type: RfqmOrderTypes.Otc,
},
orderHash: '0x01234567',
status: RfqmJobStatus.PendingLastLookAccepted,
updatedAt: new Date(),
workerAddress: '',
workflow: 'rfqm',
});
const mockTransactionRequest: providers.TransactionRequest = {};
const mockTransaction = new RfqmV2TransactionSubmissionEntity({
from: '0xworkeraddress',
maxFeePerGas: new BigNumber(100000),
maxPriorityFeePerGas: new BigNumber(100),
nonce: 0,
orderHash: '0x01234567',
to: '0xexchangeproxyaddress',
transactionHash: '0xsignedtransactionhash',
type: RfqmTransactionSubmissionType.Trade,
});
const mockTransactionReceipt: providers.TransactionReceipt = {
to: '0xexchangeproxyaddress',
from: '0xworkeraddress',
contractAddress: '0xexchangeproxyaddress',
transactionIndex: 0,
gasUsed: EthersBigNumber.from(10000),
logsBloom: '',
blockHash: '0xblockhash',
transactionHash: '0xsignedtransactionhash',
logs: [],
blockNumber: 1,
confirmations: 3,
cumulativeGasUsed: EthersBigNumber.from(1000),
effectiveGasPrice: EthersBigNumber.from(1000),
byzantium: true,
type: 2,
status: 1,
};
const mockNonce = 0;
const gasStationAttendantMock = mock(GasStationAttendantEthereum);
when(gasStationAttendantMock.getExpectedTransactionGasRateAsync()).thenResolve(
new BigNumber(10).shiftedBy(GWEI_DECIMALS),
);
const mockDbUtils = mock(RfqmDbUtils);
when(
mockDbUtils.findV2TransactionSubmissionsByOrderHashAsync(
'0x01234567',
deepEqual([RfqmTransactionSubmissionType.Trade]),
),
).thenResolve([]);
const updateRfqmJobCalledArgs: RfqmJobEntity[] = [];
when(mockDbUtils.updateRfqmJobAsync(anything())).thenCall(async (jobArg) => {
updateRfqmJobCalledArgs.push(_.cloneDeep(jobArg));
});
const writeV2RfqmTransactionSubmissionToDbCalledArgs: RfqmTransactionSubmissionEntity[] = [];
when(mockDbUtils.writeV2RfqmTransactionSubmissionToDbAsync(anything())).thenCall(
async (transactionArg) => {
writeV2RfqmTransactionSubmissionToDbCalledArgs.push(_.cloneDeep(transactionArg));
return _.cloneDeep(mockTransaction);
},
);
when(
mockDbUtils.findV2TransactionSubmissionByTransactionHashAsync('0xsignedtransactionhash'),
).thenResolve(_.cloneDeep(mockTransaction));
const updateRfqmTransactionSubmissionsCalledArgs: RfqmTransactionSubmissionEntity[][] = [];
when(mockDbUtils.updateRfqmTransactionSubmissionsAsync(anything())).thenCall(async (tranactionArg) => {
updateRfqmTransactionSubmissionsCalledArgs.push(_.cloneDeep(tranactionArg));
});
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.getNonceAsync('0xworkeraddress')).thenResolve(mockNonce);
when(mockBlockchainUtils.estimateGasForAsync(anything())).thenResolve(100);
when(
mockBlockchainUtils.transformTxDataToTransactionRequest(anything(), anything(), anything()),
).thenReturn(mockTransactionRequest);
when(mockBlockchainUtils.signTransactionAsync(anything())).thenResolve({
signedTransaction: 'signedTransaction',
transactionHash: '0xsignedtransactionhash',
});
when(mockBlockchainUtils.getExchangeProxyAddress()).thenReturn('0xexchangeproxyaddress');
when(mockBlockchainUtils.submitSignedTransactionAsync(anything())).thenResolve(
'0xsignedtransactionhash',
);
when(mockBlockchainUtils.getReceiptsAsync(deepEqual(['0xsignedtransactionhash']))).thenResolve([
mockTransactionReceipt,
]);
when(mockBlockchainUtils.getCurrentBlockAsync()).thenResolve(4);
when(mockBlockchainUtils.createAccessListForAsync(anything())).thenResolve({
accessList: {
'0x1234': ['0x0'],
'0x12345': ['0x1'],
},
gasEstimate: 1000,
});
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
gasStationAttendant: instance(gasStationAttendantMock),
rfqBlockchainUtils: instance(mockBlockchainUtils),
enableAccessList: true,
});
const callback = async (
newSubmissionContextStatus: SubmissionContextStatus,
oldSubmissionContextStatus?: SubmissionContextStatus,
): Promise<void> => {
if (newSubmissionContextStatus !== oldSubmissionContextStatus) {
const newJobStatus =
SubmissionContext.tradeSubmissionContextStatusToJobStatus(newSubmissionContextStatus);
job.status = newJobStatus;
await mockDbUtils.updateRfqmJobAsync(job);
}
};
await rfqmService.submitToChainAsync({
kind: job.kind,
to: '0xexchangeproxyaddress',
from: '0xworkeraddress',
calldata: '0xcalldata',
expiry: job.expiry,
identifier: job.orderHash,
submissionType: RfqmTransactionSubmissionType.Trade,
onSubmissionContextStatusUpdate: callback,
});
verify(mockBlockchainUtils.estimateGasForAsync(anything()));
verify(mockBlockchainUtils.createAccessListForAsync(anything())).once();
expect(job.status).to.equal(RfqmJobStatus.SucceededConfirmed);
expect(writeV2RfqmTransactionSubmissionToDbCalledArgs[0].status).to.equal(
RfqmTransactionSubmissionStatus.Presubmit,
);
expect(updateRfqmTransactionSubmissionsCalledArgs[0][0].status).to.equal(
RfqmTransactionSubmissionStatus.Submitted,
);
expect(updateRfqmTransactionSubmissionsCalledArgs[1][0].status).to.equal(
RfqmTransactionSubmissionStatus.SucceededConfirmed,
);
});
it('should call createAccessListForAsync and should not affect the overall method when RPC errors out', async () => {
const nowS = Math.round(new Date().getTime() / ONE_SECOND_MS);
const job = new RfqmV2JobEntity({
affiliateAddress: '',
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(nowS + 600),
fee: {
amount: '0',
token: '',
type: 'fixed',
},
integratorId: '',
lastLookResult: true,
makerUri: 'http://foo.bar',
order: {
order: {
chainId: '1',
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
new BigNumber(nowS + 600),
new BigNumber(1),
new BigNumber(1),
).toString(),
maker: '0xmaker',
makerAmount: '1000000',
makerToken: '0xmakertoken',
taker: '0xtaker',
takerAmount: '10000000',
takerToken: '0xtakertoken',
txOrigin: '',
verifyingContract: '',
},
type: RfqmOrderTypes.Otc,
},
orderHash: '0x01234567',
status: RfqmJobStatus.PendingLastLookAccepted,
updatedAt: new Date(),
workerAddress: '',
workflow: 'rfqm',
});
const mockTransactionRequest: providers.TransactionRequest = {};
const mockTransaction = new RfqmV2TransactionSubmissionEntity({
from: '0xworkeraddress',
maxFeePerGas: new BigNumber(100000),
maxPriorityFeePerGas: new BigNumber(100),
nonce: 0,
orderHash: '0x01234567',
to: '0xexchangeproxyaddress',
transactionHash: '0xsignedtransactionhash',
type: RfqmTransactionSubmissionType.Trade,
});
const mockTransactionReceipt: providers.TransactionReceipt = {
to: '0xto',
from: '0xfrom',
contractAddress: '0xexchangeproxyaddress',
transactionIndex: 0,
gasUsed: EthersBigNumber.from(10000),
logsBloom: '',
blockHash: '0xblockhash',
transactionHash: '0xsignedtransactionhash',
logs: [],
blockNumber: 1,
confirmations: 3,
cumulativeGasUsed: EthersBigNumber.from(1000),
effectiveGasPrice: EthersBigNumber.from(1000),
byzantium: true,
type: 2,
status: 1,
};
const mockNonce = 0;
const gasStationAttendantMock = mock(GasStationAttendantEthereum);
when(gasStationAttendantMock.getExpectedTransactionGasRateAsync()).thenResolve(
new BigNumber(10).shiftedBy(GWEI_DECIMALS),
);
const mockDbUtils = mock(RfqmDbUtils);
when(
mockDbUtils.findV2TransactionSubmissionsByOrderHashAsync(
'0x01234567',
deepEqual([RfqmTransactionSubmissionType.Trade]),
),
).thenResolve([]);
const updateRfqmJobCalledArgs: RfqmJobEntity[] = [];
when(mockDbUtils.updateRfqmJobAsync(anything())).thenCall(async (jobArg) => {
updateRfqmJobCalledArgs.push(_.cloneDeep(jobArg));
});
const writeV2RfqmTransactionSubmissionToDbCalledArgs: RfqmTransactionSubmissionEntity[] = [];
when(mockDbUtils.writeV2RfqmTransactionSubmissionToDbAsync(anything())).thenCall(
async (transactionArg) => {
writeV2RfqmTransactionSubmissionToDbCalledArgs.push(_.cloneDeep(transactionArg));
return _.cloneDeep(mockTransaction);
},
);
when(
mockDbUtils.findV2TransactionSubmissionByTransactionHashAsync('0xsignedtransactionhash'),
).thenResolve(_.cloneDeep(mockTransaction));
const updateRfqmTransactionSubmissionsCalledArgs: RfqmTransactionSubmissionEntity[][] = [];
when(mockDbUtils.updateRfqmTransactionSubmissionsAsync(anything())).thenCall(async (tranactionArg) => {
updateRfqmTransactionSubmissionsCalledArgs.push(_.cloneDeep(tranactionArg));
});
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.getNonceAsync('0xworkeraddress')).thenResolve(mockNonce);
when(mockBlockchainUtils.estimateGasForAsync(anything())).thenResolve(100);
when(
mockBlockchainUtils.transformTxDataToTransactionRequest(anything(), anything(), anything()),
).thenReturn(mockTransactionRequest);
when(mockBlockchainUtils.signTransactionAsync(anything())).thenResolve({
signedTransaction: 'signedTransaction',
transactionHash: '0xsignedtransactionhash',
});
when(mockBlockchainUtils.getExchangeProxyAddress()).thenReturn('0xexchangeproxyaddress');
when(mockBlockchainUtils.submitSignedTransactionAsync(anything())).thenResolve(
'0xsignedtransactionhash',
);
when(mockBlockchainUtils.getReceiptsAsync(deepEqual(['0xsignedtransactionhash']))).thenResolve([
mockTransactionReceipt,
]);
when(mockBlockchainUtils.getCurrentBlockAsync()).thenResolve(4);
when(mockBlockchainUtils.createAccessListForAsync(anything())).thenReject(new Error('error'));
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
gasStationAttendant: instance(gasStationAttendantMock),
rfqBlockchainUtils: instance(mockBlockchainUtils),
enableAccessList: true,
});
const callback = async (
newSubmissionContextStatus: SubmissionContextStatus,
oldSubmissionContextStatus?: SubmissionContextStatus,
): Promise<void> => {
if (newSubmissionContextStatus !== oldSubmissionContextStatus) {
const newJobStatus =
SubmissionContext.tradeSubmissionContextStatusToJobStatus(newSubmissionContextStatus);
job.status = newJobStatus;
await mockDbUtils.updateRfqmJobAsync(job);
}
};
await rfqmService.submitToChainAsync({
kind: job.kind,
to: '0xexchangeproxyaddress',
from: '0xworkeraddress',
calldata: '0xcalldata',
expiry: job.expiry,
identifier: job.orderHash,
submissionType: RfqmTransactionSubmissionType.Trade,
onSubmissionContextStatusUpdate: callback,
});
verify(mockBlockchainUtils.estimateGasForAsync(anything()));
verify(mockBlockchainUtils.createAccessListForAsync(anything())).once();
expect(job.status).to.equal(RfqmJobStatus.SucceededConfirmed);
expect(writeV2RfqmTransactionSubmissionToDbCalledArgs[0].status).to.equal(
RfqmTransactionSubmissionStatus.Presubmit,
);
expect(updateRfqmTransactionSubmissionsCalledArgs[0][0].status).to.equal(
RfqmTransactionSubmissionStatus.Submitted,
);
expect(updateRfqmTransactionSubmissionsCalledArgs[1][0].status).to.equal(
RfqmTransactionSubmissionStatus.SucceededConfirmed,
);
});
});
// Not all tests from test block 'kind is `rfqm_v2_job`' is included here as most of the tests are similiar.
// The tests below specifically test if corresponding methods are called for job kind `meta_transaction_job`.
describe('kind is `meta_transaction_job`', () => {
it('submits a transaction successfully when there is no previous transaction', async () => {
const nowS = Math.round(new Date().getTime() / ONE_SECOND_MS);
const jobId = 'jobId';
const transactionSubmissionId = 'submissionId';
const job = createMetaTransactionJobEntity(
{
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(nowS + 600),
fee: {
amount: new BigNumber(0),
token: '',
type: 'fixed',
},
inputToken: '0xinputToken',
inputTokenAmount: new BigNumber(10),
integratorId: '0xintegrator',
metaTransaction: MOCK_META_TRANSACTION,
metaTransactionHash: MOCK_META_TRANSACTION.getHash(),
minOutputTokenAmount: new BigNumber(10),
outputToken: '0xoutputToken',
takerAddress: '0xtakerAddress',
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
status: RfqmJobStatus.PendingLastLookAccepted,
workerAddress: '0xworkeraddress',
},
jobId,
);
const mockTransactionRequest: providers.TransactionRequest = {};
const mockTransaction = createMetaTransactionSubmissionEntity(
{
from: '0xworkeraddress',
metaTransactionJobId: jobId,
maxFeePerGas: new BigNumber(100000),
maxPriorityFeePerGas: new BigNumber(100),
nonce: 0,
to: '0xexchangeproxyaddress',
transactionHash: '0xsignedtransactionhash',
type: RfqmTransactionSubmissionType.Trade,
},
transactionSubmissionId,
);
const mockTransactionReceipt: providers.TransactionReceipt = {
to: '0xto',
from: '0xfrom',
contractAddress: '0xexchangeproxyaddress',
transactionIndex: 0,
gasUsed: EthersBigNumber.from(10000),
logsBloom: '',
blockHash: '0xblockhash',
transactionHash: '0xsignedtransactionhash',
logs: [],
blockNumber: 1,
confirmations: 3,
cumulativeGasUsed: EthersBigNumber.from(1000),
effectiveGasPrice: EthersBigNumber.from(1000),
byzantium: true,
type: 2,
status: 1,
};
const mockNonce = 0;
const gasStationAttendantMock = mock(GasStationAttendantEthereum);
when(gasStationAttendantMock.getExpectedTransactionGasRateAsync()).thenResolve(
new BigNumber(10).shiftedBy(GWEI_DECIMALS),
);
const mockDbUtils = mock(RfqmDbUtils);
when(
mockDbUtils.findMetaTransactionSubmissionsByJobIdAsync(
jobId,
deepEqual([RfqmTransactionSubmissionType.Trade]),
),
).thenResolve([]);
const updateRfqmJobCalledArgs: MetaTransactionJobEntity[] = [];
when(mockDbUtils.updateRfqmJobAsync(anything())).thenCall(async (jobArg) => {
updateRfqmJobCalledArgs.push(_.cloneDeep(jobArg));
});
const writeMetaTransactionSubmissionAsyncCalledArgs: MetaTransactionSubmissionEntity[] = [];
when(mockDbUtils.writeMetaTransactionSubmissionAsync(anything())).thenCall(async (transactionArg) => {
writeMetaTransactionSubmissionAsyncCalledArgs.push(_.cloneDeep(transactionArg));
return _.cloneDeep(mockTransaction);
});
when(
mockDbUtils.findMetaTransactionSubmissionsByTransactionHashAsync(
'0xsignedtransactionhash',
RfqmTransactionSubmissionType.Trade,
),
).thenResolve([_.cloneDeep(mockTransaction)]);
const updateRfqmTransactionSubmissionsCalledArgs: MetaTransactionSubmissionEntity[][] = [];
when(mockDbUtils.updateRfqmTransactionSubmissionsAsync(anything())).thenCall(async (tranactionArg) => {
updateRfqmTransactionSubmissionsCalledArgs.push(_.cloneDeep(tranactionArg));
});
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.getNonceAsync('0xworkeraddress')).thenResolve(mockNonce);
when(mockBlockchainUtils.estimateGasForAsync(anything())).thenResolve(100);
when(
mockBlockchainUtils.transformTxDataToTransactionRequest(anything(), anything(), anything()),
).thenReturn(mockTransactionRequest);
when(mockBlockchainUtils.signTransactionAsync(anything())).thenResolve({
signedTransaction: 'signedTransaction',
transactionHash: '0xsignedtransactionhash',
});
when(mockBlockchainUtils.getExchangeProxyAddress()).thenReturn('0xexchangeproxyaddress');
when(mockBlockchainUtils.submitSignedTransactionAsync(anything())).thenResolve(
'0xsignedtransactionhash',
);
when(mockBlockchainUtils.getReceiptsAsync(deepEqual(['0xsignedtransactionhash']))).thenResolve([
mockTransactionReceipt,
]);
when(mockBlockchainUtils.getCurrentBlockAsync()).thenResolve(4);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
gasStationAttendant: instance(gasStationAttendantMock),
rfqBlockchainUtils: instance(mockBlockchainUtils),
});
const callback = async (
newSubmissionContextStatus: SubmissionContextStatus,
oldSubmissionContextStatus?: SubmissionContextStatus,
): Promise<void> => {
if (newSubmissionContextStatus !== oldSubmissionContextStatus) {
const newJobStatus =
SubmissionContext.tradeSubmissionContextStatusToJobStatus(newSubmissionContextStatus);
job.status = newJobStatus;
await mockDbUtils.updateRfqmJobAsync(job);
}
};
await rfqmService.submitToChainAsync({
kind: job.kind,
to: '0xexchangeproxyaddress',
from: '0xworkeraddress',
calldata: '0xcalldata',
expiry: job.expiry,
identifier: job.id,
submissionType: RfqmTransactionSubmissionType.Trade,
onSubmissionContextStatusUpdate: callback,
});
verify(mockBlockchainUtils.estimateGasForAsync(anything()));
// eth_createAccessList should not be called when not enabled
verify(mockBlockchainUtils.createAccessListForAsync(anything())).never();
expect(job.status).to.equal(RfqmJobStatus.SucceededConfirmed);
expect(writeMetaTransactionSubmissionAsyncCalledArgs[0].status).to.equal(
RfqmTransactionSubmissionStatus.Presubmit,
);
expect(updateRfqmTransactionSubmissionsCalledArgs[0][0].status).to.equal(
RfqmTransactionSubmissionStatus.Submitted,
);
expect(updateRfqmTransactionSubmissionsCalledArgs[1][0].status).to.equal(
RfqmTransactionSubmissionStatus.SucceededConfirmed,
);
});
it('recovers a PRESUBMIT transaction which actually submitted', async () => {
const nowS = Math.round(new Date().getTime() / ONE_SECOND_MS);
const jobId = 'jobId';
const transactionSubmissionId = 'submissionId';
const job = createMetaTransactionJobEntity(
{
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(nowS + 600),
fee: {
amount: new BigNumber(0),
token: '',
type: 'fixed',
},
inputToken: '0xinputToken',
inputTokenAmount: new BigNumber(10),
integratorId: '0xintegrator',
metaTransaction: MOCK_META_TRANSACTION,
metaTransactionHash: MOCK_META_TRANSACTION.getHash(),
minOutputTokenAmount: new BigNumber(10),
outputToken: '0xoutputToken',
takerAddress: '0xtakerAddress',
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
status: RfqmJobStatus.PendingLastLookAccepted,
workerAddress: '0xworkeraddress',
},
jobId,
);
const mockPresubmitTransaction = createMetaTransactionSubmissionEntity(
{
from: '0xworkeraddress',
metaTransactionJobId: jobId,
maxFeePerGas: new BigNumber(100000),
maxPriorityFeePerGas: new BigNumber(100),
nonce: 0,
status: RfqmTransactionSubmissionStatus.Presubmit,
to: '0xexchangeproxyaddress',
transactionHash: '0xpresubmittransactionhash',
type: RfqmTransactionSubmissionType.Trade,
},
transactionSubmissionId,
);
const mockTransactionReceipt: providers.TransactionReceipt = {
blockHash: '0xblockhash',
blockNumber: 1,
byzantium: true,
confirmations: 3,
contractAddress: '0xexchangeproxyaddress',
cumulativeGasUsed: EthersBigNumber.from(1000),
effectiveGasPrice: EthersBigNumber.from(1000),
from: '0xworkeraddress',
gasUsed: EthersBigNumber.from(10000),
logs: [],
logsBloom: '',
status: 1,
to: '0xexchangeproxyaddress',
transactionHash: '0xpresubmittransactionhash',
transactionIndex: 0,
type: 2,
};
const mockTransactionResponse: providers.TransactionResponse = {
chainId: 1,
confirmations: 0,
data: '',
from: '0xworkeraddress',
gasLimit: EthersBigNumber.from(1000000),
hash: '0xpresubmittransactionhash',
nonce: 0,
type: 2,
value: EthersBigNumber.from(0),
wait: (_confirmations: number | undefined) => Promise.resolve(mockTransactionReceipt),
};
const mockMinedBlock: providers.Block = {
_difficulty: EthersBigNumber.from(2),
difficulty: 2,
extraData: '',
gasLimit: EthersBigNumber.from(1000),
gasUsed: EthersBigNumber.from(1000),
hash: '0xblockhash',
miner: '0xminer',
nonce: '0x000',
number: 21,
parentHash: '0xparentblockhash',
timestamp: 12345,
transactions: ['0xpresubmittransactionhash'],
};
const mockNonce = 0;
const gasStationAttendantMock = mock(GasStationAttendantEthereum);
when(gasStationAttendantMock.getExpectedTransactionGasRateAsync()).thenResolve(
new BigNumber(10).shiftedBy(GWEI_DECIMALS),
);
const mockDbUtils = mock(RfqmDbUtils);
when(
mockDbUtils.findMetaTransactionSubmissionsByJobIdAsync(
jobId,
deepEqual([RfqmTransactionSubmissionType.Trade]),
),
).thenResolve([mockPresubmitTransaction]);
const updateRfqmTransactionSubmissionsCalledArgs: MetaTransactionSubmissionEntity[][] = [];
when(mockDbUtils.updateRfqmTransactionSubmissionsAsync(anything())).thenCall(async (tranactionArg) => {
updateRfqmTransactionSubmissionsCalledArgs.push(_.cloneDeep(tranactionArg));
});
const mockBlockchainUtils = mock(RfqBlockchainUtils);
// This mock response indicates that the transaction is present in the mempool
when(mockBlockchainUtils.getTransactionAsync('0xpresubmittransactionhash')).thenResolve(
mockTransactionResponse,
);
when(mockBlockchainUtils.getNonceAsync('0xworkeraddress')).thenResolve(mockNonce);
when(mockBlockchainUtils.estimateGasForAsync(anything())).thenReject(
new Error('estimateGasForAsync called during recovery'),
);
when(mockBlockchainUtils.getReceiptsAsync(deepEqual(['0xpresubmittransactionhash']))).thenResolve([
mockTransactionReceipt,
]);
when(mockBlockchainUtils.getBlockAsync('0xblockhash')).thenResolve(mockMinedBlock);
when(mockBlockchainUtils.getCurrentBlockAsync()).thenResolve(4);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
gasStationAttendant: instance(gasStationAttendantMock),
rfqBlockchainUtils: instance(mockBlockchainUtils),
});
const callback = async (
newSubmissionContextStatus: SubmissionContextStatus,
oldSubmissionContextStatus?: SubmissionContextStatus,
): Promise<void> => {
if (newSubmissionContextStatus !== oldSubmissionContextStatus) {
const newJobStatus =
SubmissionContext.tradeSubmissionContextStatusToJobStatus(newSubmissionContextStatus);
job.status = newJobStatus;
await mockDbUtils.updateRfqmJobAsync(job);
}
};
await rfqmService.submitToChainAsync({
kind: job.kind,
to: '0xexchangeproxyaddress',
from: '0xworkeraddress',
calldata: '0xcalldata',
expiry: job.expiry,
identifier: job.id,
submissionType: RfqmTransactionSubmissionType.Trade,
onSubmissionContextStatusUpdate: callback,
});
// eth_createAccessList should not be called when not enabled
verify(mockBlockchainUtils.createAccessListForAsync(anything())).never();
// Logic should first check to see if the transaction was actually sent.
// If it was (and it is being mock so in this test) then the logic first
// updates the status of the transaction to "Submitted"
expect(updateRfqmTransactionSubmissionsCalledArgs[0][0].status).to.equal(
RfqmTransactionSubmissionStatus.Submitted,
);
// The logic then enters the watch loop. On the first check, a transaction
// receipt exists for this transaction and it will be marked "confirmed"
expect(updateRfqmTransactionSubmissionsCalledArgs[1][0].status).to.equal(
RfqmTransactionSubmissionStatus.SucceededConfirmed,
);
expect(job.status).to.equal(RfqmJobStatus.SucceededConfirmed);
});
it('throws exception if there are more than 1 transaction submissions with the same transaction hash', async () => {
const nowS = Math.round(new Date().getTime() / ONE_SECOND_MS);
const jobId = 'jobId';
const transactionSubmissionId = 'submissionId';
const job = createMetaTransactionJobEntity(
{
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(nowS + 600),
fee: {
amount: new BigNumber(0),
token: '',
type: 'fixed',
},
inputToken: '0xinputToken',
inputTokenAmount: new BigNumber(10),
integratorId: '0xintegrator',
metaTransaction: MOCK_META_TRANSACTION,
metaTransactionHash: MOCK_META_TRANSACTION.getHash(),
minOutputTokenAmount: new BigNumber(10),
outputToken: '0xoutputToken',
takerAddress: '0xtakerAddress',
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
status: RfqmJobStatus.PendingLastLookAccepted,
workerAddress: '0xworkeraddress',
},
jobId,
);
const mockTransactionRequest: providers.TransactionRequest = {};
const mockTransaction = createMetaTransactionSubmissionEntity(
{
from: '0xworkeraddress',
metaTransactionJobId: jobId,
maxFeePerGas: new BigNumber(100000),
maxPriorityFeePerGas: new BigNumber(100),
nonce: 0,
to: '0xexchangeproxyaddress',
transactionHash: '0xsignedtransactionhash',
type: RfqmTransactionSubmissionType.Trade,
},
transactionSubmissionId,
);
const mockTransactionReceipt: providers.TransactionReceipt = {
to: '0xto',
from: '0xfrom',
contractAddress: '0xexchangeproxyaddress',
transactionIndex: 0,
gasUsed: EthersBigNumber.from(10000),
logsBloom: '',
blockHash: '0xblockhash',
transactionHash: '0xsignedtransactionhash',
logs: [],
blockNumber: 1,
confirmations: 3,
cumulativeGasUsed: EthersBigNumber.from(1000),
effectiveGasPrice: EthersBigNumber.from(1000),
byzantium: true,
type: 2,
status: 1,
};
const mockNonce = 0;
const gasStationAttendantMock = mock(GasStationAttendantEthereum);
when(gasStationAttendantMock.getExpectedTransactionGasRateAsync()).thenResolve(
new BigNumber(10).shiftedBy(GWEI_DECIMALS),
);
const mockDbUtils = mock(RfqmDbUtils);
when(
mockDbUtils.findMetaTransactionSubmissionsByJobIdAsync(
jobId,
deepEqual([RfqmTransactionSubmissionType.Trade]),
),
).thenResolve([]);
const updateRfqmJobCalledArgs: MetaTransactionJobEntity[] = [];
when(mockDbUtils.updateRfqmJobAsync(anything())).thenCall(async (jobArg) => {
updateRfqmJobCalledArgs.push(_.cloneDeep(jobArg));
});
const writeMetaTransactionSubmissionAsyncCalledArgs: MetaTransactionSubmissionEntity[] = [];
when(mockDbUtils.writeMetaTransactionSubmissionAsync(anything())).thenCall(async (transactionArg) => {
writeMetaTransactionSubmissionAsyncCalledArgs.push(_.cloneDeep(transactionArg));
return _.cloneDeep(mockTransaction);
});
when(
mockDbUtils.findMetaTransactionSubmissionsByTransactionHashAsync(
'0xsignedtransactionhash',
RfqmTransactionSubmissionType.Trade,
),
).thenResolve([_.cloneDeep(mockTransaction), _.cloneDeep(mockTransaction)]);
const updateRfqmTransactionSubmissionsCalledArgs: MetaTransactionSubmissionEntity[][] = [];
when(mockDbUtils.updateRfqmTransactionSubmissionsAsync(anything())).thenCall(async (tranactionArg) => {
updateRfqmTransactionSubmissionsCalledArgs.push(_.cloneDeep(tranactionArg));
});
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.getNonceAsync('0xworkeraddress')).thenResolve(mockNonce);
when(mockBlockchainUtils.estimateGasForAsync(anything())).thenResolve(100);
when(
mockBlockchainUtils.transformTxDataToTransactionRequest(anything(), anything(), anything()),
).thenReturn(mockTransactionRequest);
when(mockBlockchainUtils.signTransactionAsync(anything())).thenResolve({
signedTransaction: 'signedTransaction',
transactionHash: '0xsignedtransactionhash',
});
when(mockBlockchainUtils.getExchangeProxyAddress()).thenReturn('0xexchangeproxyaddress');
when(mockBlockchainUtils.submitSignedTransactionAsync(anything())).thenResolve(
'0xsignedtransactionhash',
);
when(mockBlockchainUtils.getReceiptsAsync(deepEqual(['0xsignedtransactionhash']))).thenResolve([
mockTransactionReceipt,
]);
when(mockBlockchainUtils.getCurrentBlockAsync()).thenResolve(4);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
gasStationAttendant: instance(gasStationAttendantMock),
rfqBlockchainUtils: instance(mockBlockchainUtils),
});
const callback = async (
newSubmissionContextStatus: SubmissionContextStatus,
oldSubmissionContextStatus?: SubmissionContextStatus,
): Promise<void> => {
if (newSubmissionContextStatus !== oldSubmissionContextStatus) {
const newJobStatus =
SubmissionContext.tradeSubmissionContextStatusToJobStatus(newSubmissionContextStatus);
job.status = newJobStatus;
await mockDbUtils.updateRfqmJobAsync(job);
}
};
try {
await rfqmService.submitToChainAsync({
kind: job.kind,
to: '0xexchangeproxyaddress',
from: '0xworkeraddress',
calldata: '0xcalldata',
expiry: job.expiry,
identifier: job.id,
submissionType: RfqmTransactionSubmissionType.Trade,
onSubmissionContextStatusUpdate: callback,
});
expect.fail();
} catch (e) {
expect(e.message).to.contain('Transaction hash have been submitted not exactly once');
}
});
});
// Not all tests from test block 'kind is `rfqm_v2_job`' is included here as most of the tests are similiar.
// The tests below specifically test if corresponding methods are called for job kind `meta_transaction_v2_job`.
describe('kind is `meta_transaction_v2_job`', () => {
it('submits a transaction successfully when there is no previous transaction', async () => {
const nowS = Math.round(new Date().getTime() / ONE_SECOND_MS);
const jobId = 'jobId';
const job = createMetaTransactionV2JobEntity(
{
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(nowS + 600),
inputToken: '0xinputToken',
inputTokenAmount: new BigNumber(10),
integratorId: '0xintegrator',
calledFunction: 'transformERC20',
metaTransaction: MOCK_META_TRANSACTION_V2,
metaTransactionHash: MOCK_META_TRANSACTION_V2.getHash(),
minOutputTokenAmount: new BigNumber(10),
outputToken: '0xoutputToken',
takerAddress: '0xtakerAddress',
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
tokens: ['0xinputToken', '0xoutputToken'],
status: RfqmJobStatus.PendingLastLookAccepted,
workerAddress: '0xworkeraddress',
},
jobId,
);
const transactionSubmissionId = 'submissionId';
const mockTransactionRequest: providers.TransactionRequest = {};
const mockTransaction = createMetaTransactionV2SubmissionEntity(
{
from: '0xworkeraddress',
metaTransactionV2JobId: jobId,
maxFeePerGas: new BigNumber(100000),
maxPriorityFeePerGas: new BigNumber(100),
nonce: 0,
to: '0xexchangeproxyaddress',
transactionHash: '0xsignedtransactionhash',
type: RfqmTransactionSubmissionType.Trade,
},
transactionSubmissionId,
);
const mockTransactionReceipt: providers.TransactionReceipt = {
to: '0xto',
from: '0xfrom',
contractAddress: '0xexchangeproxyaddress',
transactionIndex: 0,
gasUsed: EthersBigNumber.from(10000),
logsBloom: '',
blockHash: '0xblockhash',
transactionHash: '0xsignedtransactionhash',
logs: [],
blockNumber: 1,
confirmations: 3,
cumulativeGasUsed: EthersBigNumber.from(1000),
effectiveGasPrice: EthersBigNumber.from(1000),
byzantium: true,
type: 2,
status: 1,
};
const mockNonce = 0;
const gasStationAttendantMock = mock(GasStationAttendantEthereum);
when(gasStationAttendantMock.getExpectedTransactionGasRateAsync()).thenResolve(
new BigNumber(10).shiftedBy(GWEI_DECIMALS),
);
const mockDbUtils = mock(RfqmDbUtils);
when(
mockDbUtils.findMetaTransactionV2SubmissionsByJobIdAsync(
jobId,
deepEqual([RfqmTransactionSubmissionType.Trade]),
),
).thenResolve([]);
const updateRfqmJobCalledArgs: MetaTransactionJobEntity[] = [];
when(mockDbUtils.updateRfqmJobAsync(anything())).thenCall(async (jobArg) => {
updateRfqmJobCalledArgs.push(_.cloneDeep(jobArg));
});
const writeMetaTransactionV2SubmissionAsyncCalledArgs: MetaTransactionSubmissionEntity[] = [];
when(mockDbUtils.writeMetaTransactionV2SubmissionAsync(anything())).thenCall(async (transactionArg) => {
writeMetaTransactionV2SubmissionAsyncCalledArgs.push(_.cloneDeep(transactionArg));
return _.cloneDeep(mockTransaction);
});
when(
mockDbUtils.findMetaTransactionV2SubmissionsByTransactionHashAsync(
'0xsignedtransactionhash',
RfqmTransactionSubmissionType.Trade,
),
).thenResolve([_.cloneDeep(mockTransaction)]);
const updateRfqmTransactionSubmissionsCalledArgs: MetaTransactionV2SubmissionEntity[][] = [];
when(mockDbUtils.updateRfqmTransactionSubmissionsAsync(anything())).thenCall(async (tranactionArg) => {
updateRfqmTransactionSubmissionsCalledArgs.push(_.cloneDeep(tranactionArg));
});
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.getNonceAsync('0xworkeraddress')).thenResolve(mockNonce);
when(mockBlockchainUtils.estimateGasForAsync(anything())).thenResolve(100);
when(
mockBlockchainUtils.transformTxDataToTransactionRequest(anything(), anything(), anything()),
).thenReturn(mockTransactionRequest);
when(mockBlockchainUtils.signTransactionAsync(anything())).thenResolve({
signedTransaction: 'signedTransaction',
transactionHash: '0xsignedtransactionhash',
});
when(mockBlockchainUtils.getExchangeProxyAddress()).thenReturn('0xexchangeproxyaddress');
when(mockBlockchainUtils.submitSignedTransactionAsync(anything())).thenResolve(
'0xsignedtransactionhash',
);
when(mockBlockchainUtils.getReceiptsAsync(deepEqual(['0xsignedtransactionhash']))).thenResolve([
mockTransactionReceipt,
]);
when(mockBlockchainUtils.getCurrentBlockAsync()).thenResolve(4);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
gasStationAttendant: instance(gasStationAttendantMock),
rfqBlockchainUtils: instance(mockBlockchainUtils),
});
const callback = async (
newSubmissionContextStatus: SubmissionContextStatus,
oldSubmissionContextStatus?: SubmissionContextStatus,
): Promise<void> => {
if (newSubmissionContextStatus !== oldSubmissionContextStatus) {
const newJobStatus =
SubmissionContext.tradeSubmissionContextStatusToJobStatus(newSubmissionContextStatus);
job.status = newJobStatus;
await mockDbUtils.updateRfqmJobAsync(job);
}
};
await rfqmService.submitToChainAsync({
kind: job.kind,
to: '0xexchangeproxyaddress',
from: '0xworkeraddress',
calldata: '0xcalldata',
expiry: job.expiry,
identifier: job.id,
submissionType: RfqmTransactionSubmissionType.Trade,
onSubmissionContextStatusUpdate: callback,
});
verify(mockBlockchainUtils.estimateGasForAsync(anything()));
// eth_createAccessList should not be called when not enabled
verify(mockBlockchainUtils.createAccessListForAsync(anything())).never();
expect(job.status).to.equal(RfqmJobStatus.SucceededConfirmed);
expect(writeMetaTransactionV2SubmissionAsyncCalledArgs[0].status).to.equal(
RfqmTransactionSubmissionStatus.Presubmit,
);
expect(updateRfqmTransactionSubmissionsCalledArgs[0][0].status).to.equal(
RfqmTransactionSubmissionStatus.Submitted,
);
expect(updateRfqmTransactionSubmissionsCalledArgs[1][0].status).to.equal(
RfqmTransactionSubmissionStatus.SucceededConfirmed,
);
});
it('recovers a PRESUBMIT transaction which actually submitted', async () => {
const nowS = Math.round(new Date().getTime() / ONE_SECOND_MS);
const jobId = 'jobId';
const transactionSubmissionId = 'submissionId';
const job = createMetaTransactionV2JobEntity(
{
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(nowS + 600),
inputToken: '0xinputToken',
inputTokenAmount: new BigNumber(10),
integratorId: '0xintegrator',
calledFunction: 'transformERC20',
metaTransaction: MOCK_META_TRANSACTION_V2,
metaTransactionHash: MOCK_META_TRANSACTION_V2.getHash(),
minOutputTokenAmount: new BigNumber(10),
outputToken: '0xoutputToken',
takerAddress: '0xtakerAddress',
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
tokens: ['0xinputToken', '0xoutputToken'],
status: RfqmJobStatus.PendingLastLookAccepted,
workerAddress: '0xworkeraddress',
},
jobId,
);
const mockPresubmitTransaction = createMetaTransactionV2SubmissionEntity(
{
from: '0xworkeraddress',
metaTransactionV2JobId: jobId,
maxFeePerGas: new BigNumber(100000),
maxPriorityFeePerGas: new BigNumber(100),
nonce: 0,
status: RfqmTransactionSubmissionStatus.Presubmit,
to: '0xexchangeproxyaddress',
transactionHash: '0xpresubmittransactionhash',
type: RfqmTransactionSubmissionType.Trade,
},
transactionSubmissionId,
);
const mockTransactionReceipt: providers.TransactionReceipt = {
blockHash: '0xblockhash',
blockNumber: 1,
byzantium: true,
confirmations: 3,
contractAddress: '0xexchangeproxyaddress',
cumulativeGasUsed: EthersBigNumber.from(1000),
effectiveGasPrice: EthersBigNumber.from(1000),
from: '0xworkeraddress',
gasUsed: EthersBigNumber.from(10000),
logs: [],
logsBloom: '',
status: 1,
to: '0xexchangeproxyaddress',
transactionHash: '0xpresubmittransactionhash',
transactionIndex: 0,
type: 2,
};
const mockTransactionResponse: providers.TransactionResponse = {
chainId: 1,
confirmations: 0,
data: '',
from: '0xworkeraddress',
gasLimit: EthersBigNumber.from(1000000),
hash: '0xpresubmittransactionhash',
nonce: 0,
type: 2,
value: EthersBigNumber.from(0),
wait: (_confirmations: number | undefined) => Promise.resolve(mockTransactionReceipt),
};
const mockMinedBlock: providers.Block = {
_difficulty: EthersBigNumber.from(2),
difficulty: 2,
extraData: '',
gasLimit: EthersBigNumber.from(1000),
gasUsed: EthersBigNumber.from(1000),
hash: '0xblockhash',
miner: '0xminer',
nonce: '0x000',
number: 21,
parentHash: '0xparentblockhash',
timestamp: 12345,
transactions: ['0xpresubmittransactionhash'],
};
const mockNonce = 0;
const gasStationAttendantMock = mock(GasStationAttendantEthereum);
when(gasStationAttendantMock.getExpectedTransactionGasRateAsync()).thenResolve(
new BigNumber(10).shiftedBy(GWEI_DECIMALS),
);
const mockDbUtils = mock(RfqmDbUtils);
when(
mockDbUtils.findMetaTransactionV2SubmissionsByJobIdAsync(
jobId,
deepEqual([RfqmTransactionSubmissionType.Trade]),
),
).thenResolve([mockPresubmitTransaction]);
const updateRfqmTransactionSubmissionsCalledArgs: MetaTransactionV2SubmissionEntity[][] = [];
when(mockDbUtils.updateRfqmTransactionSubmissionsAsync(anything())).thenCall(async (tranactionArg) => {
updateRfqmTransactionSubmissionsCalledArgs.push(_.cloneDeep(tranactionArg));
});
const mockBlockchainUtils = mock(RfqBlockchainUtils);
// This mock response indicates that the transaction is present in the mempool
when(mockBlockchainUtils.getTransactionAsync('0xpresubmittransactionhash')).thenResolve(
mockTransactionResponse,
);
when(mockBlockchainUtils.getNonceAsync('0xworkeraddress')).thenResolve(mockNonce);
when(mockBlockchainUtils.estimateGasForAsync(anything())).thenReject(
new Error('estimateGasForAsync called during recovery'),
);
when(mockBlockchainUtils.getReceiptsAsync(deepEqual(['0xpresubmittransactionhash']))).thenResolve([
mockTransactionReceipt,
]);
when(mockBlockchainUtils.getBlockAsync('0xblockhash')).thenResolve(mockMinedBlock);
when(mockBlockchainUtils.getCurrentBlockAsync()).thenResolve(4);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
gasStationAttendant: instance(gasStationAttendantMock),
rfqBlockchainUtils: instance(mockBlockchainUtils),
});
const callback = async (
newSubmissionContextStatus: SubmissionContextStatus,
oldSubmissionContextStatus?: SubmissionContextStatus,
): Promise<void> => {
if (newSubmissionContextStatus !== oldSubmissionContextStatus) {
const newJobStatus =
SubmissionContext.tradeSubmissionContextStatusToJobStatus(newSubmissionContextStatus);
job.status = newJobStatus;
await mockDbUtils.updateRfqmJobAsync(job);
}
};
await rfqmService.submitToChainAsync({
kind: job.kind,
to: '0xexchangeproxyaddress',
from: '0xworkeraddress',
calldata: '0xcalldata',
expiry: job.expiry,
identifier: job.id,
submissionType: RfqmTransactionSubmissionType.Trade,
onSubmissionContextStatusUpdate: callback,
});
// eth_createAccessList should not be called when not enabled
verify(mockBlockchainUtils.createAccessListForAsync(anything())).never();
// Logic should first check to see if the transaction was actually sent.
// If it was (and it is being mock so in this test) then the logic first
// updates the status of the transaction to "Submitted"
expect(updateRfqmTransactionSubmissionsCalledArgs[0][0].status).to.equal(
RfqmTransactionSubmissionStatus.Submitted,
);
// The logic then enters the watch loop. On the first check, a transaction
// receipt exists for this transaction and it will be marked "confirmed"
expect(updateRfqmTransactionSubmissionsCalledArgs[1][0].status).to.equal(
RfqmTransactionSubmissionStatus.SucceededConfirmed,
);
expect(job.status).to.equal(RfqmJobStatus.SucceededConfirmed);
});
it('throws exception if there are more than 1 transaction submissions with the same transaction hash', async () => {
const nowS = Math.round(new Date().getTime() / ONE_SECOND_MS);
const jobId = 'jobId';
const transactionSubmissionId = 'submissionId';
const job = createMetaTransactionV2JobEntity(
{
chainId: 1,
createdAt: new Date(),
expiry: new BigNumber(nowS + 600),
inputToken: '0xinputToken',
inputTokenAmount: new BigNumber(10),
integratorId: '0xintegrator',
calledFunction: 'transformERC20',
metaTransaction: MOCK_META_TRANSACTION_V2,
metaTransactionHash: MOCK_META_TRANSACTION_V2.getHash(),
minOutputTokenAmount: new BigNumber(10),
outputToken: '0xoutputToken',
takerAddress: '0xtakerAddress',
takerSignature: {
signatureType: SignatureType.EthSign,
v: 27,
r: '0x01',
s: '0x02',
},
tokens: ['0xinputToken', '0xoutputToken'],
status: RfqmJobStatus.PendingLastLookAccepted,
workerAddress: '0xworkeraddress',
},
jobId,
);
const mockTransactionRequest: providers.TransactionRequest = {};
const mockTransaction = createMetaTransactionV2SubmissionEntity(
{
from: '0xworkeraddress',
metaTransactionV2JobId: jobId,
maxFeePerGas: new BigNumber(100000),
maxPriorityFeePerGas: new BigNumber(100),
nonce: 0,
to: '0xexchangeproxyaddress',
transactionHash: '0xsignedtransactionhash',
type: RfqmTransactionSubmissionType.Trade,
},
transactionSubmissionId,
);
const mockTransactionReceipt: providers.TransactionReceipt = {
to: '0xto',
from: '0xfrom',
contractAddress: '0xexchangeproxyaddress',
transactionIndex: 0,
gasUsed: EthersBigNumber.from(10000),
logsBloom: '',
blockHash: '0xblockhash',
transactionHash: '0xsignedtransactionhash',
logs: [],
blockNumber: 1,
confirmations: 3,
cumulativeGasUsed: EthersBigNumber.from(1000),
effectiveGasPrice: EthersBigNumber.from(1000),
byzantium: true,
type: 2,
status: 1,
};
const mockNonce = 0;
const gasStationAttendantMock = mock(GasStationAttendantEthereum);
when(gasStationAttendantMock.getExpectedTransactionGasRateAsync()).thenResolve(
new BigNumber(10).shiftedBy(GWEI_DECIMALS),
);
const mockDbUtils = mock(RfqmDbUtils);
when(
mockDbUtils.findMetaTransactionV2SubmissionsByJobIdAsync(
jobId,
deepEqual([RfqmTransactionSubmissionType.Trade]),
),
).thenResolve([]);
const updateRfqmJobCalledArgs: MetaTransactionJobEntity[] = [];
when(mockDbUtils.updateRfqmJobAsync(anything())).thenCall(async (jobArg) => {
updateRfqmJobCalledArgs.push(_.cloneDeep(jobArg));
});
const writeMetaTransactionV2SubmissionAsyncCalledArgs: MetaTransactionV2SubmissionEntity[] = [];
when(mockDbUtils.writeMetaTransactionV2SubmissionAsync(anything())).thenCall(async (transactionArg) => {
writeMetaTransactionV2SubmissionAsyncCalledArgs.push(_.cloneDeep(transactionArg));
return _.cloneDeep(mockTransaction);
});
when(
mockDbUtils.findMetaTransactionV2SubmissionsByTransactionHashAsync(
'0xsignedtransactionhash',
RfqmTransactionSubmissionType.Trade,
),
).thenResolve([_.cloneDeep(mockTransaction), _.cloneDeep(mockTransaction)]);
const updateRfqmTransactionSubmissionsCalledArgs: MetaTransactionV2SubmissionEntity[][] = [];
when(mockDbUtils.updateRfqmTransactionSubmissionsAsync(anything())).thenCall(async (tranactionArg) => {
updateRfqmTransactionSubmissionsCalledArgs.push(_.cloneDeep(tranactionArg));
});
const mockBlockchainUtils = mock(RfqBlockchainUtils);
when(mockBlockchainUtils.getNonceAsync('0xworkeraddress')).thenResolve(mockNonce);
when(mockBlockchainUtils.estimateGasForAsync(anything())).thenResolve(100);
when(
mockBlockchainUtils.transformTxDataToTransactionRequest(anything(), anything(), anything()),
).thenReturn(mockTransactionRequest);
when(mockBlockchainUtils.signTransactionAsync(anything())).thenResolve({
signedTransaction: 'signedTransaction',
transactionHash: '0xsignedtransactionhash',
});
when(mockBlockchainUtils.getExchangeProxyAddress()).thenReturn('0xexchangeproxyaddress');
when(mockBlockchainUtils.submitSignedTransactionAsync(anything())).thenResolve(
'0xsignedtransactionhash',
);
when(mockBlockchainUtils.getReceiptsAsync(deepEqual(['0xsignedtransactionhash']))).thenResolve([
mockTransactionReceipt,
]);
when(mockBlockchainUtils.getCurrentBlockAsync()).thenResolve(4);
const rfqmService = buildWorkerServiceForUnitTest({
dbUtils: instance(mockDbUtils),
gasStationAttendant: instance(gasStationAttendantMock),
rfqBlockchainUtils: instance(mockBlockchainUtils),
});
const callback = async (
newSubmissionContextStatus: SubmissionContextStatus,
oldSubmissionContextStatus?: SubmissionContextStatus,
): Promise<void> => {
if (newSubmissionContextStatus !== oldSubmissionContextStatus) {
const newJobStatus =
SubmissionContext.tradeSubmissionContextStatusToJobStatus(newSubmissionContextStatus);
job.status = newJobStatus;
await mockDbUtils.updateRfqmJobAsync(job);
}
};
try {
await rfqmService.submitToChainAsync({
kind: job.kind,
to: '0xexchangeproxyaddress',
from: '0xworkeraddress',
calldata: '0xcalldata',
expiry: job.expiry,
identifier: job.id,
submissionType: RfqmTransactionSubmissionType.Trade,
onSubmissionContextStatusUpdate: callback,
});
expect.fail();
} catch (e) {
expect(e.message).to.contain('Transaction hash have been submitted not exactly once');
}
});
});
});
});