74dff2b412
Co-authored-by: Phil Liao <phil@0x.org>
8420 lines
383 KiB
TypeScript
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');
|
|
}
|
|
});
|
|
});
|
|
});
|
|
});
|