Repository: ecadlabs/taquito (ts)

Issue:
confirmationPollingTimeoutSecond not working
**Description**
There is an issue with the `confirmationPollingTimeoutSecond` on the TezosToolkit. During the operation confirmation (both on the contract and wallet  API), the timer for the timeout seems to restart to 0 on every emission of a new block.

**Steps To Reproduce**

Execute the following code:
```
Tezos.setProvider({config: {confirmationPollingTimeoutSecond:15}})
const op = await Tezos.contract.originate({
        balance: "1",
        code: `parameter string;
        storage string;
        code {CAR;
              PUSH string "Hello ";
              CONCAT;
              NIL operation; PAIR};
        `,
        init: `"test"`,
        gasLimit:800000
})
await op.confirmation()
```
**Expected behavior**
We set a high `gasLimit`, so the operation will not be included in a block. We set a timeout of 15 seconds and expect that an error `'Confirmation polling timed out'` will be thrown after 15 seconds. The error is not thrown.

**Desktop (please complete the following information):**
 - Version 14.0.0



Pull request description:
fix(confirmationPollingTimeoutSecond): fix for contract/wallet API
The timeout was not taken into consideration on both the contract and wallet API when confirming the operations. In cases where an operation was not found, our integration tests would never end instead of throwing a timeout error.

fix #2006

Thank you for your contribution to Taquito.

Before submitting this PR, please make sure:

- [ ] Your code builds cleanly without any errors or warnings
- [ ] You have added unit tests
- [ ] You have added integration tests (if relevant/appropriate)
- [ ] All public methods or types have TypeDoc coverage with a complete description, and ideally an @example
- [ ] You have added or updated corresponding documentation
- [ ] If relevant, you have written a first draft summary describing the change for inclusion in Release Notes. 

## Release Note Draft Snippet

__If relevant, please write a summary of your change that will be suitable for
inclusion in the Release Notes for the next Taquito release.__


Patch:
diff --git a/packages/taquito/src/operations/operations.ts b/packages/taquito/src/operations/operations.ts
index 0ee775372..5456a6458 100644
--- a/packages/taquito/src/operations/operations.ts
+++ b/packages/taquito/src/operations/operations.ts
@@ -3,7 +3,7 @@ import {
   OperationContentsAndResult,
   OperationContentsAndResultReveal,
 } from '@taquito/rpc';
-import { defer, EMPTY, of, range, ReplaySubject, throwError } from 'rxjs';
+import { BehaviorSubject, defer, EMPTY, of, range, ReplaySubject, throwError } from 'rxjs';
 import {
   catchError,
   concatMap,
@@ -36,6 +36,11 @@ export class Operation {
 
   private currentHead$ = this._pollingConfig$.pipe(
     switchMap((config) => {
+      return new BehaviorSubject(config).pipe(
+        timeoutWith(config.timeout * 1000, throwError(new Error('Confirmation polling timed out')))
+      );
+    }),
+    switchMap(() => {
       return defer(() =>
         createObservableFromSubscription(this.context.stream.subscribeBlock('head'))
       ).pipe(
@@ -46,8 +51,7 @@ export class Operation {
             endWith(newHead)
           );
         }),
-        tap((newHead) => (this.lastHead = newHead)),
-        timeoutWith(config.timeout * 1000, throwError(new Error('Confirmation polling timed out')))
+        tap((newHead) => (this.lastHead = newHead))
       );
     }),
     shareReplay({ refCount: true })
diff --git a/packages/taquito/src/wallet/operation-factory.ts b/packages/taquito/src/wallet/operation-factory.ts
index 73f86dbbc..8f661f355 100644
--- a/packages/taquito/src/wallet/operation-factory.ts
+++ b/packages/taquito/src/wallet/operation-factory.ts
@@ -1,6 +1,24 @@
 import { BlockResponse } from '@taquito/rpc';
-import { concat, defer, from, Observable, of, range, SchedulerLike, throwError } from 'rxjs';
-import { concatMap, shareReplay, startWith, switchMap, timeoutWith } from 'rxjs/operators';
+import {
+  BehaviorSubject,
+  concat,
+  defer,
+  from,
+  Observable,
+  of,
+  range,
+  SchedulerLike,
+  throwError,
+} from 'rxjs';
+import {
+  concatMap,
+  mergeMap,
+  publishReplay,
+  refCount,
+  startWith,
+  switchMap,
+  timeoutWith,
+} from 'rxjs/operators';
 import { Context } from '../context';
 import { BlockIdentifier } from '../read-provider/interface';
 import { createObservableFromSubscription } from '../subscribe/create-observable-from-subscription';
@@ -10,21 +28,24 @@ import { WalletOperation } from './operation';
 import { OriginationWalletOperation } from './origination-operation';
 import { TransactionWalletOperation } from './transaction-operation';
 
+export function timeoutAfter<T>(timeoutMillisec: number): (source: Observable<T>) => Observable<T> {
+  return function inner(source: Observable<T>): Observable<T> {
+    return new BehaviorSubject(null).pipe(
+      timeoutWith(timeoutMillisec, throwError(new Error('Confirmation polling timed out'))),
+      mergeMap(() => source)
+    );
+  };
+}
+
 export const createNewPollingBasedHeadObservable = (
   sharedHeadOb: Observable<BlockResponse>,
   context: Context,
-  scheduler?: SchedulerLike
+  _scheduler?: SchedulerLike
 ): Observable<BlockResponse> => {
   return sharedHeadOb.pipe(
-    timeoutWith(
-      context.config.confirmationPollingTimeoutSecond * 1000,
-      throwError(new Error('Confirmation polling timed out')),
-      scheduler
-    ),
-    shareReplay({
-      refCount: true,
-      scheduler,
-    })
+    timeoutAfter(context.config.confirmationPollingTimeoutSecond * 1000),
+    publishReplay(1),
+    refCount()
   );
 };
 
diff --git a/packages/taquito/src/wallet/operation.ts b/packages/taquito/src/wallet/operation.ts
index 420dae83a..439da54be 100644
--- a/packages/taquito/src/wallet/operation.ts
+++ b/packages/taquito/src/wallet/operation.ts
@@ -8,7 +8,8 @@ import {
   filter,
   first,
   map,
-  shareReplay,
+  publishReplay,
+  refCount,
   switchMap,
   takeWhile,
   tap,
@@ -41,7 +42,8 @@ export class WalletOperation {
       );
     }),
     tap((newHead) => (this.lastHead = newHead)),
-    shareReplay({ bufferSize: 1, refCount: true })
+    publishReplay(1),
+    refCount()
   );
 
   // Observable that emit once operation is seen in a block
@@ -64,7 +66,8 @@ export class WalletOperation {
       return typeof x !== 'undefined';
     }),
     first(),
-    shareReplay({ bufferSize: 1, refCount: true })
+    publishReplay(1),
+    refCount()
   );
 
   async operationResults() {


Test patch:
diff --git a/integration-tests/config.ts b/integration-tests/config.ts
index 561bd3307..9135cc936 100644
--- a/integration-tests/config.ts
+++ b/integration-tests/config.ts
@@ -10,6 +10,8 @@ import { knownContractPtJakart2, knownBigMapContractPtJakart2, knownTzip12BigMap
 
 const nodeCrypto = require('crypto');
 
+export const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
+
 if (typeof jest !== 'undefined') {
   jest.setTimeout(60000 * 10);
 }
diff --git a/integration-tests/contract-confirmation-timeout.spec.ts b/integration-tests/contract-confirmation-timeout.spec.ts
new file mode 100644
index 000000000..3ecc1436b
--- /dev/null
+++ b/integration-tests/contract-confirmation-timeout.spec.ts
@@ -0,0 +1,38 @@
+import BigNumber from "bignumber.js";
+import { CONFIGS, sleep } from "./config";
+
+CONFIGS().forEach(({ lib, rpc, setup }) => {
+  const Tezos = lib;
+  let timeBetweenBlocks: BigNumber;
+  describe(`Test confirmationPollingTimeoutSecond with contract API using: ${rpc}`, () => {
+
+    beforeEach(async (done) => {
+      await setup();
+      timeBetweenBlocks = (await Tezos.rpc.getConstants()).delay_increment_per_round ?? new BigNumber(15);
+      done()
+    })
+
+    it('Verify a timeout error is thrown when an operation is never confirmed', async (done) => {
+      const timeout = Number(timeBetweenBlocks.multipliedBy(2));
+      Tezos.setProvider({ config: { confirmationPollingTimeoutSecond: timeout } })
+      expect(async () => {
+        const op = await Tezos.contract.originate({
+          code: `parameter string;
+          storage string;
+          code {CAR;
+                PUSH string "Hello ";
+                CONCAT;
+                NIL operation; PAIR};
+          `,
+          init: `"test"`,
+          gasLimit: 600000 // gas limit too high, the operation won't be included in a block
+        })
+        await op.confirmation()
+      }).rejects.toThrow('Confirmation polling timed out');
+
+      await sleep(timeout * 2000);
+
+      done();
+    })
+  });
+})
diff --git a/integration-tests/contract-handling-missed-blocks.spec.ts b/integration-tests/contract-handling-missed-blocks.spec.ts
index 2d812ffe6..570f8bf22 100644
--- a/integration-tests/contract-handling-missed-blocks.spec.ts
+++ b/integration-tests/contract-handling-missed-blocks.spec.ts
@@ -1,19 +1,23 @@
 import { PollingSubscribeProvider } from "@taquito/taquito";
+import BigNumber from "bignumber.js";
 import { CONFIGS } from "./config";
 
 CONFIGS().forEach(({ lib, rpc, setup }) => {
   const Tezos = lib;
-   describe(`Test handling of missed blocks through contract api using: ${rpc}`, () => {
+  let timeBetweenBlocks: BigNumber;
+  describe(`Test handling of missed blocks through contract api using: ${rpc}`, () => {
 
     beforeEach(async (done) => {
-      await setup()
+      await setup();
+      timeBetweenBlocks = (await Tezos.rpc.getConstants()).delay_increment_per_round ?? new BigNumber(15);
       done()
     })
+
     it('Verify the operation is found even if the poller skipped blocks', async (done) => {
       // To simulate the behavior where blocks are skipped, we use a polling interval higher than the time between blocks
       // Before fixing issue #1783, if the block containing the operation was skipped, the operation was never found, and the test ended with a timeout
       // After fixing issue #1783, if blocks are skipped, they will be retrieved and inspected by the Operation class
-      Tezos.setStreamProvider(Tezos.getFactory(PollingSubscribeProvider)({ pollingIntervalMilliseconds: 60000 }));
+      Tezos.setStreamProvider(Tezos.getFactory(PollingSubscribeProvider)({ pollingIntervalMilliseconds: Number(timeBetweenBlocks.multipliedBy(4000)) }));
       const op = await Tezos.contract.transfer({ to: 'tz1ZfrERcALBwmAqwonRXYVQBDT9BjNjBHJu', amount: 2 });
       await op.confirmation();
       expect(op.includedInBlock).toBeLessThan(Number.POSITIVE_INFINITY);
diff --git a/integration-tests/polling-subscribe-provider.spec.ts b/integration-tests/polling-subscribe-provider.spec.ts
index 9150cb07b..c3117f518 100644
--- a/integration-tests/polling-subscribe-provider.spec.ts
+++ b/integration-tests/polling-subscribe-provider.spec.ts
@@ -1,8 +1,6 @@
-import { CONFIGS } from "./config";
+import { CONFIGS, sleep } from "./config";
 import { Protocols, PollingSubscribeProvider } from "@taquito/taquito";
 
-const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
-
 CONFIGS().forEach(({ lib, rpc, setup, protocol }) => {
   const Tezos = lib;
   const kathmandunetAndMondaynet = (protocol === Protocols.PtKathman || protocol === Protocols.ProtoALpha) ? test: test.skip;
diff --git a/integration-tests/wallet-confirmation-timeout.spec.ts b/integration-tests/wallet-confirmation-timeout.spec.ts
new file mode 100644
index 000000000..0fd355dbb
--- /dev/null
+++ b/integration-tests/wallet-confirmation-timeout.spec.ts
@@ -0,0 +1,40 @@
+import BigNumber from "bignumber.js";
+import { CONFIGS, sleep } from "./config";
+
+CONFIGS().forEach(({ lib, rpc, setup }) => {
+  const Tezos = lib;
+  let timeBetweenBlocks: BigNumber;
+  describe(`Test confirmationPollingTimeoutSecond with wallet API using: ${rpc}`, () => {
+
+    beforeEach(async (done) => {
+      await setup();
+      timeBetweenBlocks = (await Tezos.rpc.getConstants()).delay_increment_per_round ?? new BigNumber(15);
+      done()
+    })
+
+    it('Verify a timeout error is thrown when an operation is never confirmed', async (done) => {
+      const timeout = Number(timeBetweenBlocks.multipliedBy(2));
+      Tezos.setProvider({ config: { confirmationPollingTimeoutSecond: timeout } })
+      expect(async () => {
+
+          const op = await Tezos.wallet.originate({
+          code: `parameter string;
+          storage string;
+          code {CAR;
+                PUSH string "Hello ";
+                CONCAT;
+                NIL operation; PAIR};
+          `,
+          init: `"test"`,
+          gasLimit: 600000 // gas limit too high, the operation won't be included in a block
+        }).send()
+        await op.confirmation()
+
+      }).rejects.toThrow('Confirmation polling timed out');
+
+      await sleep(timeout * 2000);
+
+      done();
+    })
+  });
+})