Is TokenPocket - Web3 & Crypto Wallet safe?

Medium risk

TokenPocket is medium risk. TokenPocket sends a persistent clientId UUID to Google Analytics on every use. Code also defines tp_tx_confirm_chain and tp_lt (unlock) events on the same transport, not captured live. No wallet keys sent; not disclosed in the listing.…

TP Global Ltdv2.17.3Chrome Web Store
45Risk

AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.

Publishers can request a review.

Findings

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-359
SourceAI SANDBOX

TokenPocket sends a persistent ID and wallet activity to Google Analytics

TokenPocket sends a persistent clientId UUID to Google Analytics on every use.

Code also defines tp_tx_confirm_chain and tp_lt (unlock) events on the same transport, not captured live.

No wallet keys sent; not disclosed in the listing.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You create a wallet, move between onboarding screens, unlock your wallet, or confirm a transaction.

The extension did this

The extension sends a named event describing that action to Google Analytics, tagged with a persistent installation ID.

This happens on every onboarding screen transition and, per the extension's own code, on every wallet unlock and transaction confirmation.

02EvidenceNETWORK CAPTURE
Captured request
POSThttps://www.google-analytics.com/mp/collect?measurement_id=G-B15TQE99Q2&api_secret=<redacted>
Google Analytics Measurement Protocol collect endpoint; no response body is used by the extension.
Headers
content-typeapplication/json
Body
{
  "client_id": "3a57ac91-6de2-494f-8ec4-7bf5d6c55314",
  "events": [
    {
      "name": "tp_start_page",
      "params": {
        "start_page": "create_account",
        "session_id": "1783561224676",
        "engagement_time_msec": 100
      }
    }
  ]
}
03EvidenceFIELD TABLE
Fields sent with every Google Analytics event
FieldValueWhy it matters
Installation ID (client_id)
3a57ac91-6de2-494f-8ec4-7bf5d6c55314A UUID generated on first run, stored locally, unchanged across sessions, letting Analytics link every event below to this install.
Session ID
1783561224676A timestamp-based identifier that resets after 30 minutes of inactivity, grouping events within one sitting.
Screen / page name
create_accountWhich onboarding or wallet screen you were on when the event fired, e.g. the create-account or unlock screen.
Chain name & ID (tp_tx_confirm_chain)
{"blockchain_id":56,"name":"BNB Smart Chain"}Per the extension's source code, every transaction confirmation reports which blockchain network you transacted on.
Keyring counts (tp_lt)
{"lt":42,"kn":3,"khn":1,"khsn":2}Per the extension's source code, every wallet unlock reports how many keyrings/accounts you have set up, split by type.
04EvidenceCODE COMPARE
The code that does this

Event transport (background.js) and the wallet-unlock / transaction-confirm callers

What it actually does
fireEvent() — deobfuscated
async fireEvent(eventName, params = {}) {
  params.session_id || (params.session_id = await this.getOrCreateSessionId());
  params.engagement_time_msec || (params.engagement_time_msec = 100);
  try {
    const res = await fetch(
      (this.debug
        ? "https://www.google-analytics.com/debug/mp/collect"
        : "https://www.google-analytics.com/mp/collect") +
        "?measurement_id=G-B15TQE99Q2&api_secret=<redacted>",
      {
        method: "POST",
        body: JSON.stringify({
          client_id: await this.getOrCreateClientId(),
          events: [{ name: eventName, params }]
        })
      }
    );
    if (!this.debug) return;
    console.log(await res.text());
  } catch (err) {
    console.error("Google Analytics request failed with an exception", err);
  }
}
getOrCreateClientId() — deobfuscated
async getOrCreateClientId() {
  let { clientId } = await chrome.storage.local.get("clientId");
  return clientId || (clientId = self.crypto.randomUUID(),
    await chrome.storage.local.set({ clientId }),
    clientId);
}
Transaction-confirm caller — deobfuscated (background.js)
analytics.fireEvent("tp_tx_confirm_chain", {
  blockchain_id: chain?.blockchain_id,
  name: chain?.name
});
Wallet-unlock caller — deobfuscated (ui.js)
eventBus.addEventListener("initKeyringsOtherChainEnd", async (payload) => {
  refreshChains();
  analytics.analyticsFireEvent("tp_lt", payload);
});
05EvidenceTHIRD PARTY LIST
Destination receiving wallet-installation telemetry
  • www.google-analytics.com

    Google's Measurement Protocol endpoint. Receives the persistent client_id, session data, and named events (page views; per source, tx-confirm, unlock) for GA property G-B15TQE99Q2.

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-359
SourceAI SANDBOX

Push opt-in binds wallet addresses to a push token

Enabling push notifications registers a push subscription, posted to TokenPocket's server.

The same path posts wallet addresses (blockchain ID, address, type, action) to preserver.mytokenpocket.vip with push token/device ID.

Not reached.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You enable push notifications in the wallet settings.

The settings screen checks that Firebase messaging and browser push support are available before requesting a token.

The extension did this

The extension binds stored wallet addresses to the push subscription it registered for your browser.

Initial registration calls the address-sync path with forceAll enabled, so every enabled address-chain pair is eligible for posting.

02EvidenceNETWORK CAPTURE
Captured request
POSThttps://preserver.mytokenpocket.vip/v1/message/bind_regtoken_addresses
No response was captured because the dynamic test did not reach the notification opt-in control.
03EvidenceFIELD TABLE
Fields constructed for the address-binding request
FieldValueWhy it matters
Blockchain ID
1 (illustrative Ethereum chain record)This tells the server which blockchain a wallet address belongs to.
Wallet address
0x742d35Cc6634C0532925a3b844Bc454e4438f44e (illustrative)This can identify a public wallet and connect balances or transactions to the push subscription.
Wallet type
0 for a non-watch wallet; 1 for a watch wallet; 9 when no type is setThis distinguishes normal, watch-only, or fallback wallet records in the request.
Push subscription token
{"endpoint":"https://fcm.googleapis.com/fcm/send/eY7r3pV4mK9sQ2aL6tN8uB1cD5fG0hJ4kL7mP9qR2sT6vW8xY3zA5bC0dE1fG","keys":{"p256dh":"BOrTQ5w6xH9c2Vf8mN1aL4zP7qS0dE3rG6uI9oK2yM5bN8pQ1tS4vW7xZ0aC3eF6gH9iJ2kL5mN8oP1qR4sT7uV0wX3yZ6","auth":"q1w2e3r4t5y6u7i8o9p0a1b2"}} (illustrative)This lets the server connect wallet addresses to the browser push subscription used for notifications.
Extension device ID
mfgccjchihfkkindfppnaooecgfneiiiThis links the address binding to the extension installation rather than only to a single wallet record.
04EvidenceCODE COMPARE
The code that does this

Notification opt-in and address binding in shipped code

What it actually does
Readable UI toggle pathdeobfuscated/462.js
onChange: async r => {
  if (!n) {
    a(!0);
    try {
      if (r) {
        if (!(0, Tt.J)()) throw new Error("Firebase messaging config missing");
        if (!("undefined" != typeof window && "Notification" in window && "serviceWorker" in navigator && "PushManager" in window)) throw new Error("Current environment does not support push notifications");
        (0, y.log)("[togglePush] start", r), (0, y.log)("[togglePush] before getFirebaseRegToken");
        const e = await pc((0, C.j)(t), 15e3, "Get Firebase registration token timeout");
        (0, y.log)("[togglePush] after getFirebaseRegToken", e), await pc(t.registerNotificationToken(e), 1e4, "Register notification token timeout"), l((0, ic.W)(await pc(t.getNotificationSettings(), 1e4, "Load notification settings timeout")))
      } else {
        const e = {
          ...o,
          pushEnabled: !1
        };
        l(e), await pc(t.disableNotificationPush(), 1e4, "Disable notification push timeout"), await pc((0, C.t)(), 1e4, "Unsubscribe push subscription timeout"), l((0, ic.W)(await pc(t.getNotificationSettings(), 1e4, "Load notification settings timeout")))
      }
    } catch (n) {
      console.error("[NotificationMessage.togglePush]", n), S.ZP.error(n?.message || e("RequestError")), l((0, ic.W)(await t.getNotificationSettings()))
    } finally {
      a(!1)
    }
  }
}
Readable push subscription serializationdeobfuscated/background.js
Cae = "BG5R2ox8rpbza2VQ1J_XPL4Eu02eYympO-_VCjUR9ob8GklAiHCax2M-EN6KhiSAdcVmJt_TvWBXB1UCRSHWlgo";
requestToken = async () => {
  if (this.log("requestToken start"), !self?.registration?.pushManager) throw this.log("requestToken no pushManager"), new Error("Push manager is not available in extension service worker");
  let e = await (self?.registration.pushManager.getSubscription());
  this.log("requestToken existing subscription", !!e), e || (this.log("requestToken subscribe start"), e = await (self?.registration.pushManager.subscribe({
    userVisibleOnly: !0,
    applicationServerKey: _ae(Cae)
  })), this.log("requestToken subscribe success")), await this.syncDebugSubscription(e.toJSON());
  const t = JSON.stringify(e.toJSON());
  return this.log("requestToken finish", {
    endpoint: e?.endpoint
  }), MS.setNotificationSettings({
    regToken: t
  }), t
};
Readable token and address binding pathdeobfuscated/background.js
Xne = async e => gU("/v1/message/newregtoken", {
  method: "post",
  data: e
});
$ne = async e => gU("/v1/message/bind_regtoken_addresses", {
  method: "post",
  data: e
});
registerToken = async e => {
  if (this.log("registerToken start"), !e) throw new Error("Missing regToken");
  const t = {
    ...this.getSettings(),
    pushEnabled: !0,
    regToken: e
  };
  return await this.registerDevice(e, t), await this.syncRegToken(e, t), await this.subscribeDefaultTopics(e), await this.syncAddresses(e, 0, {
    forceAll: !0
  }), MS.setNotificationSettings({
    regToken: e,
    pushEnabled: !0
  }), this.log("registerToken finish"), this.getSettings()
};
syncRegToken = async (e, t = this.getSettings()) => {
  this.log("syncRegToken"), await Xne({
    device_id: MS.getExtensionId(),
    reg_token: e,
    push_status: t.pushEnabled ? 1 : 0,
    transfer_status: t.transferStatus ? 1 : 0,
    watch_status: t.watchStatus ? 1 : 0,
    activity_status: t.activityStatus ? 1 : 0,
    ts: Date.now()
  }), MS.setNotificationSettings({
    lastSyncAt: Date.now()
  })
};
getAccountWalletAddressParams = async (e, t) => {
  const r = await wae.getSupportChains(),
    n = new Map;
  r.forEach((e => {
    n.set(e.enum, e), n.set(e.native_token_id, e)
  }));
  const i = new Map;
  return MS.getAccountEnableChainsData(e).map((e => n.get(e))).filter(Boolean).forEach((r => {
    const n = this.getAccountChainAddresses(e, r);
    r?.blockchain_id && n.length && !String(r.native_token_id).includes("PublicKey") && n.forEach((n => {
      const a = {
        blockchain_id: Number(r.blockchain_id),
        address: n,
        wallet_type: this.getWalletType(e),
        action: t
      };
      i.set(this.getAddressKey(a), a)
    }))
  })), Array.from(i.values())
};
syncAddressParams = async (e, t) => {
  if (!t.length) return;
  this.log("syncAddressParams paramsList", t);
  const r = await $ne({
    params_list: t,
    reg_token: e,
    device_id: MS.getExtensionId()
  });
  this.log("syncAddressParams bind result", JSON.stringify(r))
};
syncAddresses = async (e, t, r = {}) => {
  this.log("syncAddresses start", {
    action: t,
    forceAll: r.forceAll
  });
  const n = this.getSettings(),
    i = Array.isArray(n.boundAddressKeys) ? n.boundAddressKeys : [],
    a = await this.getAllWalletAddressParams(t),
    o = 0 === t ? a.map((e => ({
      ...e,
      action: 0
    }))) : [],
    s = 0 === t ? (({
      boundAddressKeys: e = [],
      currentAddressParams: t = [],
      action: r = 1
    }) => {
      const n = new Set(t.map((e => Oae(e))));
      return e.filter((e => !n.has(e))).map((e => Nae(e, r))).filter(Boolean)
    })({
      boundAddressKeys: i,
      currentAddressParams: o,
      action: 1
    }) : [];
  s.length && (this.log("syncAddresses staleParamsList", s), await this.syncAddressParams(e, s), this.updateBoundAddressKeys(s, 1));
  let c = 0 !== t || r.forceAll ? a : a.filter((e => !i.includes(this.getAddressKey(e))));
  if (1 === t && r.forceAll && i.length) {
    const e = new Map;
    i.forEach((t => {
      const r = Nae(t, 1);
      r && e.set(this.getAddressKey(r), r)
    })), a.forEach((t => {
      e.set(this.getAddressKey(t), t)
    })), c = Array.from(e.values())
  }
  c.length ? (await this.syncAddressParams(e, c), this.updateBoundAddressKeys(c, t), this.log("syncAddresses finish")) : this.log("syncAddresses no address params", {
    allParamsCount: a.length,
    boundAddressCount: i.length,
    staleParamsCount: s.length
  })
};
05EvidenceTHIRD PARTY LIST
Services involved in the push-registration flow
  • preserver.mytokenpocket.vip

    TokenPocket server that receives the push token and wallet-address binding request.

  • fcmregistrations.googleapis.com

    Google Firebase Cloud Messaging service used by the browser push-registration flow.

  • firebaseinstallations.googleapis.com

    Google Firebase Installations service used by Firebase messaging to manage installation identity.

06EvidencePLAIN NOTE
Traffic caveat

Dynamic analysis did not reach the notification settings screen within the available test sessions, so it observed no requests to the two TokenPocket push-registration endpoints. The shipped code still contains the opt-in path and the request construction shown above.

Updated 17 September 2026mfgccjchihfkkindfppnaooecgfneiii