Repository: onflow/fcl-js (js)

Issue:
[BUG] <Access API Error after upgrading fcl-js>
### Current Behavior

The problem we're seeing happenes right after we upgraded fcl-js sdk to latest stable version:
```
@onflow/fcl": "1.9.0"   ->  "1.10.0"
@onflow/transport-http": "^1.9.0"   -> "1.10.0"
@onflow/transport-grpc": "1.3.1"    ->  "1.4.0"
```

The access node provider we're using is Quicknode, e.g.: https://dawn-wild-valley.flow-mainnet.quiknode.pro/4beb5df392b61695757c57d6b82474193c427fb5/


* Request after upgrade:
```
Request URL:
https://dawn-wild-valley.flow-mainnet.quiknode.pro/v1/accounts/a886e89209bca080?block_height=sealed&expand=contracts,keys
Request Method:
GET
Status Code:
401 Unauthorized
Remote Address:
64.31.42.222:443
Referrer Policy:
strict-origin-when-cross-origin
```

* Comparing with previous version and expected behavior below you can see that the API token in the access url is gone, suspecting it to be some url parsing bug introduced between versions.


### Expected Behavior

* Request before update:
```
Request URL:
https://dawn-wild-valley.flow-mainnet.quiknode.pro/4beb5df392b61695757c57d6b82474193c427fb5/v1/accounts/a886e89209bca080?block_height=sealed&expand=contracts,keys
Request Method:
GET
Status Code:
200 OK
Remote Address:
64.31.42.222:443
Referrer Policy:
strict-origin-when-cross-origin
```

### Steps To Reproduce

```
@onflow/fcl": "1.9.0"   ->  "1.10.0"
@onflow/transport-http": "^1.9.0"   -> "1.10.0"
@onflow/transport-grpc": "1.3.1"    ->  "1.4.0"
```

Nothing else changed. (Though Quicknode endpoints come with hostname whitelists, `localhost:1024` is one within the whitelist for test.)

### Environment

```markdown
- OS: MacOS 14.4.1 (23E224)
- Node: 16.20.2
- npm: 8.19.4
```


### What are you currently working on that this is blocking?

It may affect app.increment.fi and other Dapps using QuickNode rpc service.

Pull request description:
Fix URL concatenation for HTTP requests
closes #1877 

Patch:
diff --git a/.changeset/beige-dodos-jam.md b/.changeset/beige-dodos-jam.md
new file mode 100644
index 00000000..4cf9cdae
--- /dev/null
+++ b/.changeset/beige-dodos-jam.md
@@ -0,0 +1,5 @@
+---
+"@onflow/transport-http": patch
+---
+
+Fix accessNode.api URL concatenation with paths in baseURL
diff --git a/.changeset/red-dodos-march.md b/.changeset/red-dodos-march.md
new file mode 100644
index 00000000..f939851d
--- /dev/null
+++ b/.changeset/red-dodos-march.md
@@ -0,0 +1,5 @@
+---
+"@onflow/fcl-wc": minor
+---
+
+Remove unnecessary client services from FCL-WC plugin
diff --git a/packages/fcl-wc/src/service.js b/packages/fcl-wc/src/service.js
index 5417e12c..e8798655 100644
--- a/packages/fcl-wc/src/service.js
+++ b/packages/fcl-wc/src/service.js
@@ -1,14 +1,13 @@
 import {WalletConnectModal} from "@walletconnect/modal"
 import {invariant} from "@onflow/util-invariant"
 import {log, LEVELS} from "@onflow/util-logger"
-import {fetchFlowWallets, isMobile, CONFIGURED_NETWORK, isIOS} from "./utils"
+import {isMobile, CONFIGURED_NETWORK, isIOS} from "./utils"
 import {FLOW_METHODS, REQUEST_TYPES} from "./constants"
 
 export const makeServicePlugin = async (client, opts = {}) => ({
   name: "fcl-plugin-service-walletconnect",
   f_type: "ServicePlugin",
   type: "discovery-service",
-  services: await makeWcServices(opts),
   serviceStrategy: {method: "WC/RPC", exec: makeExec(client, opts)},
 })
 
@@ -211,8 +210,8 @@ async function connectWc({
 
   const projectId = client.opts.projectId
   const web3Modal = new WalletConnectModal({
-    projectId
-  });
+    projectId,
+  })
 
   try {
     const {uri, approval} = await client.connect({
@@ -271,31 +270,3 @@ async function connectWc({
     web3Modal.closeModal()
   }
 }
-
-const makeBaseWalletConnectService = includeBaseWC => {
-  return {
-    f_type: "Service",
-    f_vsn: "1.0.0",
-    type: "authn",
-    method: "WC/RPC",
-    uid: "https://walletconnect.com",
-    endpoint: "flow_authn",
-    optIn: !includeBaseWC,
-    provider: {
-      address: null,
-      name: "WalletConnect",
-      icon: "https://avatars.githubusercontent.com/u/37784886",
-      description: "WalletConnect Base Service",
-      website: "https://walletconnect.com",
-      color: null,
-      supportEmail: null,
-    },
-  }
-}
-
-async function makeWcServices({projectId, includeBaseWC, wallets}) {
-  const wcBaseService = makeBaseWalletConnectService(includeBaseWC)
-  const flowWcWalletServices = (await fetchFlowWallets(projectId)) ?? []
-  const injectedWalletServices = CONFIGURED_NETWORK !== "mainnet" ? wallets : []
-  return [wcBaseService, ...flowWcWalletServices, ...injectedWalletServices]
-}
diff --git a/packages/transport-http/src/combine-urls.ts b/packages/transport-http/src/combine-urls.ts
new file mode 100644
index 00000000..2f6a035a
--- /dev/null
+++ b/packages/transport-http/src/combine-urls.ts
@@ -0,0 +1,41 @@
+/**
+ * This file was taken from the Axios project and modified to work with TypeScript/ES6 modules.
+ *
+ * The original file can be found on GitHub:
+ * https://github.com/axios/axios/blob/fe7d09bb08fa1c0e414956b7fc760c80459b0a43/lib/helpers/combineURLs.js
+ *
+ * The original license is included below:
+ *
+ * Copyright (c) 2014 Matt Zabriskie
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+/**
+ * Creates a new URL by combining the specified URLs
+ *
+ * @param baseURL The base URL
+ * @param relativeURL The relative URL
+ * @returns The combined URL
+ */
+export function combineURLs(baseURL: string, relativeURL?: string): string {
+  return relativeURL
+    ? baseURL.replace(/\/+$/, "") + "/" + relativeURL.replace(/^\/+/, "")
+    : baseURL
+}
diff --git a/packages/transport-http/src/http-request.js b/packages/transport-http/src/http-request.js
index 61964312..e7612996 100644
--- a/packages/transport-http/src/http-request.js
+++ b/packages/transport-http/src/http-request.js
@@ -1,6 +1,7 @@
 import * as logger from "@onflow/util-logger"
 import fetchTransport from "cross-fetch"
 import {safeParseJSON} from "./utils"
+import {combineURLs} from "./combine-urls"
 
 const AbortController =
   globalThis.AbortController || require("abort-controller")
@@ -70,7 +71,7 @@ export async function httpRequest({
       controller.abort()
     }, timeoutLimit)
 
-    return fetchTransport(new URL(path, hostname).toString(), {
+    return fetchTransport(combineURLs(hostname, path).toString(), {
       method: method,
       body: bodyJSON,
       headers,


Test patch:
diff --git a/packages/transport-http/src/combine-urls.test.ts b/packages/transport-http/src/combine-urls.test.ts
new file mode 100644
index 00000000..e07c074c
--- /dev/null
+++ b/packages/transport-http/src/combine-urls.test.ts
@@ -0,0 +1,72 @@
+import {combineURLs} from "./combine-urls"
+
+describe("combineUrls", () => {
+  test("base url (no slash, no path), relative url (no slash)", () => {
+    const baseURL = "https://example.com"
+    const relativeURL = "users"
+    const combinedURL = combineURLs(baseURL, relativeURL)
+    expect(combinedURL).toBe("https://example.com/users")
+  })
+
+  test("base url (no slash, no path), relative url (with slash)", () => {
+    const baseURL = "https://example.com"
+    const relativeURL = "/users"
+    const combinedURL = combineURLs(baseURL, relativeURL)
+    expect(combinedURL).toBe("https://example.com/users")
+  })
+
+  test("base url (no slash, with path), relative url (no slash)", () => {
+    const baseURL = "https://example.com/api"
+    const relativeURL = "/users"
+    const combinedURL = combineURLs(baseURL, relativeURL)
+    expect(combinedURL).toBe("https://example.com/api/users")
+  })
+
+  test("base url (no slash, with path), relative url (with slash)", () => {
+    const baseURL = "https://example.com/api"
+    const relativeURL = "/users"
+    const combinedURL = combineURLs(baseURL, relativeURL)
+    expect(combinedURL).toBe("https://example.com/api/users")
+  })
+
+  test("base url (with slash, no path), relative url (no slash)", () => {
+    const baseURL = "https://example.com/"
+    const relativeURL = "users"
+    const combinedURL = combineURLs(baseURL, relativeURL)
+    expect(combinedURL).toBe("https://example.com/users")
+  })
+
+  test("base url (with slash, no path), relative url (with slash)", () => {
+    const baseURL = "https://example.com/"
+    const relativeURL = "/users"
+    const combinedURL = combineURLs(baseURL, relativeURL)
+    expect(combinedURL).toBe("https://example.com/users")
+  })
+
+  test("base url (with slash, with path), relative url (no slash)", () => {
+    const baseURL = "https://example.com/api/"
+    const relativeURL = "users"
+    const combinedURL = combineURLs(baseURL, relativeURL)
+    expect(combinedURL).toBe("https://example.com/api/users")
+  })
+
+  test("base url (with slash, with path), relative url (with slash)", () => {
+    const baseURL = "https://example.com/api/"
+    const relativeURL = "/users"
+    const combinedURL = combineURLs(baseURL, relativeURL)
+    expect(combinedURL).toBe("https://example.com/api/users")
+  })
+
+  test("base url (with slash, with path), relative url (with slash)", () => {
+    const baseURL = "https://example.com/api/"
+    const relativeURL = "/users"
+    const combinedURL = combineURLs(baseURL, relativeURL)
+    expect(combinedURL).toBe("https://example.com/api/users")
+  })
+
+  test("base url, no relative url", () => {
+    const baseURL = "https://example.com/api"
+    const combinedURL = combineURLs(baseURL)
+    expect(combinedURL).toBe("https://example.com/api")
+  })
+})