Skip to content

Commit

Permalink
Merge pull request #536 from connext/cancel-fulfill-hotfix
Browse files Browse the repository at this point in the history
cancel fulfill hotfix
  • Loading branch information
LayneHaber authored Nov 17, 2021
2 parents 573191e + cbf1f3e commit b431493
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 14 deletions.
9 changes: 9 additions & 0 deletions packages/router/src/lib/errors/cancel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,12 @@ export class ReceiverTxExists extends NxtpError {
super(`Receiver Tx ${transactionId} already exists on chain ${chainId}`, context, "ReceiverTxExists");
}
}

export class SenderTxTooNew extends NxtpError {
static getMessage(transactionId: string, chainId: number, preparedTime: number, currentTime: number): string {
return `Sender tx ${transactionId} on chain ${chainId} too recent, preparedTime: ${preparedTime} currentTime: ${currentTime}`;
}
constructor(transactionId: string, chainId: number, preparedTime: number, currentTime: number, context: any = {}) {
super(SenderTxTooNew.getMessage(transactionId, chainId, preparedTime, currentTime), context, "SenderTxTooNew");
}
}
23 changes: 22 additions & 1 deletion packages/router/src/lib/operations/cancel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import { getContext } from "../../router";
import { ParamsInvalid, ReceiverTxExists } from "../errors";
import { CancelInput, CancelInputSchema } from "../entities";
import { TransactionStatus } from "../../adapters/subgraph/graphqlsdk";
import { SenderTxTooNew } from "../errors/cancel";

export const SENDER_PREPARE_BUFFER_TIME = 60 * 13; // 13 mins (780s)
// bsc has 3s block time, is often given 250 lag blocks

export const cancel = async (
invariantData: InvariantTransactionData,
Expand All @@ -20,7 +24,7 @@ export const cancel = async (
): Promise<providers.TransactionReceipt | undefined> => {
const { requestContext, methodContext } = createLoggingContext(cancel.name, _requestContext);

const { logger, contractWriter, contractReader } = getContext();
const { logger, contractWriter, contractReader, txService } = getContext();
logger.info("Method start", requestContext, methodContext, { invariantData, input });

// Validate InvariantData schema
Expand Down Expand Up @@ -68,6 +72,23 @@ export const cancel = async (
currentTime,
});
}

// prepare at 1000, 1000 > 2000 - 750
const preparedBlock = await txService.getBlock(invariantData.sendingChainId, preparedBlockNumber);
if (currentTime < preparedBlock.timestamp + SENDER_PREPARE_BUFFER_TIME) {
throw new SenderTxTooNew(
invariantData.transactionId,
invariantData.sendingChainId,
preparedBlock.timestamp,
currentTime,
{
requestContext,
methodContext,
preparedBlock,
currentTime,
},
);
}
} else {
cancelChain = invariantData.receivingChainId;
}
Expand Down
35 changes: 32 additions & 3 deletions packages/router/test/lib/operations/cancel.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import { expect, invariantDataMock, txReceiptMock, createLoggingContext, mkBytes32 } from "@connext/nxtp-utils";
import {
expect,
invariantDataMock,
txReceiptMock,
createLoggingContext,
mkBytes32,
getNtpTimeSeconds,
} from "@connext/nxtp-utils";

import { cancelInputMock } from "../../utils";
import { contractReaderMock, contractWriterMock } from "../../globalTestHook";
import { cancel } from "../../../src/lib/operations/cancel";
import { contractReaderMock, contractWriterMock, txServiceMock } from "../../globalTestHook";
import { cancel, SENDER_PREPARE_BUFFER_TIME } from "../../../src/lib/operations/cancel";
import { SinonStub } from "sinon";
import { SenderTxTooNew } from "../../../src/lib/errors/cancel";

const { requestContext } = createLoggingContext("TEST", undefined, mkBytes32("0xabc"));

Expand All @@ -23,6 +31,23 @@ describe("Cancel Sender Operation", () => {
);
});

it("should error if sender prepare tx is too recent", async () => {
const _cancelInputMock = { ...cancelInputMock };
const preparedTime = Math.floor(Date.now() / 1000);
(contractReaderMock.getTransactionForChain as SinonStub).resolves(undefined);
txServiceMock.getBlock.resolves({
timestamp: preparedTime,
} as any);
await expect(cancel(invariantDataMock, _cancelInputMock, requestContext)).to.eventually.be.rejectedWith(
SenderTxTooNew.getMessage(
invariantDataMock.transactionId,
invariantDataMock.sendingChainId,
preparedTime,
await getNtpTimeSeconds(),
),
);
});

it("happy: should cancel for receiver chain", async () => {
const receipt = await cancel(invariantDataMock, { ...cancelInputMock, side: "receiver" }, requestContext);

Expand All @@ -44,7 +69,11 @@ describe("Cancel Sender Operation", () => {
});

it("happy: should cancel for sender chain", async () => {
const time = Math.floor(Date.now() / 1000) - SENDER_PREPARE_BUFFER_TIME - 500;
(contractReaderMock.getTransactionForChain as SinonStub).resolves(undefined);
txServiceMock.getBlock.resolves({
timestamp: time,
} as any);
const receipt = await cancel(invariantDataMock, cancelInputMock, requestContext);

expect(receipt).to.deep.eq(txReceiptMock);
Expand Down
18 changes: 18 additions & 0 deletions packages/txservice/src/chainreader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,24 @@ export class ChainReader {
}
}

/**
* Gets a block
*
* @param chainId - The ID of the chain for which this call is related.
* @returns block representing the specified
*/
public async getBlock(
chainId: number,
blockHashOrBlockTag: providers.BlockTag | Promise<providers.BlockTag>,
): Promise<providers.Block> {
const result = await this.getProvider(chainId).getBlock(blockHashOrBlockTag);
if (result.isErr()) {
throw result.error;
} else {
return result.value;
}
}

/**
* Gets a trsanction receipt by hash
*
Expand Down
34 changes: 24 additions & 10 deletions packages/txservice/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,15 +306,11 @@ export class ChainRpcProvider {
} else if (gasStations.length > 0 && gasPrice) {
// TODO: Remove this unnecessary provider call, used temporarily for debugging / metrics.
const providerQuote = await this.provider.getGasPrice();
this.logger.debug("Retrieved gas prices",
requestContext,
methodContext,
{
chainId: this.chainId,
gasStationQuote: gasPrice,
providerQuote,
}
);
this.logger.debug("Retrieved gas prices", requestContext, methodContext, {
chainId: this.chainId,
gasStationQuote: gasPrice,
providerQuote,
});
}

// If we did not have a gas station API to use, or the gas station failed, use the provider's getGasPrice method.
Expand Down Expand Up @@ -345,7 +341,11 @@ export class ChainRpcProvider {
}

let hitMaximum = false;
if (gasPriceMaxIncreaseScalar !== undefined && gasPriceMaxIncreaseScalar > 100 && this.lastUsedGasPrice !== undefined) {
if (
gasPriceMaxIncreaseScalar !== undefined &&
gasPriceMaxIncreaseScalar > 100 &&
this.lastUsedGasPrice !== undefined
) {
// If we have a configured cap scalar, and the gas price is greater than that cap, set it to the cap.
const max = this.lastUsedGasPrice.mul(gasPriceMaxIncreaseScalar).div(100);
if (gasPrice.gt(max)) {
Expand Down Expand Up @@ -417,6 +417,20 @@ export class ChainRpcProvider {
});
}

/**
* Gets the current block number.
*
* @returns A number representing the current block number.
*/
public getBlock(
blockHashOrBlockTag: providers.BlockTag | Promise<providers.BlockTag>,
): ResultAsync<providers.Block, TransactionError> {
return this.resultWrapper<providers.Block>(false, async () => {
const block = await this.provider.getBlock(blockHashOrBlockTag);
return block;
});
}

/**
* Gets the current blocktime.
*
Expand Down

0 comments on commit b431493

Please sign in to comment.