Is UniSat Wallet safe?
UniSat Wallet is medium risk. When a dApp asks UniSat Wallet to sign Bitcoin PSBTs, it parses inputs before signing completes. For each it builds a txid:vout outpoint string and posts the list to wallet-api.unisat.io, checking for inscriptions, runes, or alkanes.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
UniSat checks PSBT input outpoints before signing
When a dApp asks UniSat Wallet to sign Bitcoin PSBTs, it parses inputs before signing completes.
For each it builds a txid:vout outpoint string and posts the list to wallet-api.unisat.io, checking for inscriptions, runes, or alkanes.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-200
- Source
- Dynamic sandbox
You reach a dApp-initiated PSBT signing prompt in UniSat Wallet.
The path is tied to signing or quick-signing a Bitcoin transaction, not to simply opening the wallet.
The extension analyzes the PSBT inputs and sends their txid:vout outpoints to UniSat's wallet API.
The remote check looks for inscriptions, runes, and alkanes associated with those inputs.
- PSBT input outpoint3f2a6b9c4d1e8f0055aa33cc77dd99112233445566778899aabbccddeeff0011:0 (illustrative)
Links the signing prompt to specific Bitcoin transaction outputs being spent. Outpoints can reveal which coins are involved.
- Active wallet addressbc1qvu0x7jacr764slykf5un4rffsk7k9dnm93nfrs
Associates wallet API requests with the Bitcoin address currently selected in the extension when that header is populated.
- Wallet API device IDQ8h4Lm2Np6Ra (illustrative)
Lets UniSat's wallet API correlate requests from the same extension installation across wallet API calls.
No request body was recorded in dynamic testing because the dApp signPsbt trigger was not reached.
The shipped code that builds outpoints and posts them to the wallet API
Wallet API method
background.jsasync getUtxoAssetsByOutpoints(e) { return this.httpClient.post("/v5/utxo/assets-by-outpoints", { outpoints: e })}Shared headers for wallet API calls
background.jsthis.updateHeaders = () => { var e = { "x-client": "UniSat Wallet", "x-version": i.UW.VERSION, "x-channel": i.UW.CHANNEL, "x-udid": i.UW.UDID, "x-udid2": i.UW.UDID2 }; this.clientAddress && (e["x-address"] = this.clientAddress), this.client.setHeaders(e)}PSBT analyzer
background.jsthis.analyzeLocalPsbts = async e => { let i = this.getNetworkType(), o = 0, a = 0, s = 0, u = !1, t = 0; var c = []; let l; try { let n = (await S.bitcoin.getFeeSummary()).list.map(e => e.feeRate); if (3 <= n.length) { let e = n[1], [t, , r] = n; l = { tooLow: Math.max(1, t - 2), tooHigh: r + 50, recommended: e } } } catch (e) { g.warn("Failed to fetch fee summary for local PSBT analysis:", e) } var d = new Set; for (let n of e) try { let e = new m.lS((0, f.Z)({ toSignData: n, networkType: i }, void 0 !== l ? { feeRateThresholds: l } : {})), t = await e.decode(), r = t.risks.some(e => e.type === A.gI.SIGHASH_NONE); r && (u = !0); var h = t.inputInfos.reduce((e, t) => e + t.value, 0); t.isCompleted && (o += t.fee, a += h, s++); for (let e of t.inputInfos) d.add("".concat(e.txid, ":").concat(e.vout)); c.push({ inputCount: t.inputInfos.length, outputCount: t.outputInfos.length, feeRate: t.feeRate, fee: t.fee, totalInput: h, isCompleted: t.isCompleted, hasSighashNone: r, parseError: !1 }) } catch (e) { g.error("Local PSBT analysis failed:", e), t++, c.push({ inputCount: 0, outputCount: 0, feeRate: "-", fee: 0, totalInput: 0, isCompleted: !1, hasSighashNone: !1, parseError: !0 }) } let r = !1; if (0 < d.size) try { r = (await S.bitcoin.getUtxoAssetsByOutpoints(Array.from(d))).some(e => 0 < e.inscriptions.length || 0 < e.runes.length || 0 < e.alkanes.length) } catch (e) { g.warn("Failed to fetch UTXO assets for PSBT analysis:", e) } return { totalFee: o, totalInput: a, completedCount: s, hasSighashNone: u, parseErrorCount: t, hasAssets: r, items: c }}Quick-sign caller
ui.jsonClickSign: () => { var e; 0 < (null == T || null == (e = T.risks) ? void 0 : e.length) ? l(!0) : O()},onQuickMultiSign: async () => { try { s(!0), await new Promise(e => setTimeout(e, 50)); var e = null != v ? v : await h.analyzeLocalPsbts(i); if (v || b(e), 0 < e.parseErrorCount) p.toastError(m("failed_to_parse_psbt")); else if (e.hasSighashNone) p.toastError(m("sighash_none_detected")); else if (e.hasAssets) p.toastError(m("quick_sign_assets_detected")); else for (let t = 0; t < i.length; t++) try { var r = i[t], n = await h.signPsbtV2(r); P(n, t) } catch (e) { x[t] = h0.FAILED, S([...x]), p.toastError("Signing failed for PSBT ".concat(t, ": " ).concat(e.message)) } } catch (e) {} finally { s(!1), y(!1) }}- wallet-api.unisat.io
UniSat wallet API endpoint that receives the PSBT input outpoint list for asset checks.
A test session created a wallet and observed normal wallet API polling, but it did not complete a connected dApp signPsbt flow. That means the shipped code path is confirmed, while a live request body for this specific endpoint was not recorded.