feat: add phantom integration
All checks were successful
build / lint (push) Successful in 11s
build / build (push) Successful in 38s
build / docker image (push) Successful in 52s

This commit is contained in:
2026-01-31 10:31:24 +00:00
parent dd8ecce947
commit e84d1cabd8
12 changed files with 234 additions and 46 deletions

View File

@@ -1,9 +1,9 @@
# PolyNote · Polygon message pusher
Send hex-encoded messages on Polygon mainnet using the Trust Wallet browser extension or in-app browser. The app builds a zero-value transaction with your message in the data field, so only gas is spent.
Send hex-encoded messages on Polygon mainnet using the Trust Wallet or Phantom browser extensions. The app builds a zero-value transaction with your message in the data field, so only gas is spent.
## What it does
- Connects to an injected EIP-1193 provider (Trust Wallet) and targets Polygon mainnet.
- Connects to injected EIP-1193 providers (Trust Wallet or Phantom) and targets Polygon mainnet.
- Hex-encodes up to 280 characters and sends a 0 MATIC transaction carrying the payload.
- Internationalization (English, Spanish, French, German, Italian, Swedish) with a locale picker and auto language detection.
- Live feedback, copy-to-clipboard for the tx hash, Polygonscan deep links, and a session “recent messages” view.

View File

@@ -593,6 +593,52 @@ button:hover:not(:disabled) {
font-size: 12px;
}
.wallet-selector {
margin-top: 12px;
padding: 14px;
border: 1px solid var(--border);
border-radius: 14px;
background: var(--surface);
}
.wallet-selector-head {
display: flex;
align-items: baseline;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 10px;
}
.wallet-toggle {
display: inline-flex;
gap: 10px;
}
.wallet-chip {
border: 1px solid var(--border);
background: var(--surface-soft);
color: var(--ink);
padding: 10px 14px;
border-radius: 12px;
font-weight: 700;
display: inline-flex;
align-items: center;
gap: 8px;
}
.wallet-chip.active {
border-color: var(--accent);
background: rgba(14, 165, 233, 0.12);
color: var(--accent-strong);
box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.15);
}
.wallet-icon {
width: 18px;
height: 18px;
display: inline-block;
}
.footer {
margin-top: 8px;
padding: 14px 18px;

View File

@@ -9,15 +9,16 @@
"light": "Heller Modus"
},
"hero": {
"eyebrow": "Eingebettetes Trust Wallet • Polygon Mainnet",
"title": "PolyNote · Trust Wallet zu Polygon",
"lede": "Verbinde dich mit der Trust-Wallet-Erweiterung oder dem In-App-Browser. Wir kodieren deinen Text in Hex und senden eine Null-MATIC-Transaktion auf Polygon nur Gasgebühren fallen an."
"eyebrow": "Trust Wallet oder Phantom • Polygon Mainnet",
"title": "PolyNote · Eingebettete Wallets zu Polygon",
"lede": "Verbinde dich mit den Erweiterungen von Trust Wallet oder Phantom. Wir kodieren deinen Text in Hex und senden eine 0-MATIC-Transaktion auf Polygon nur Gasgebühren fallen an."
},
"connection": {
"title": "Verbindung",
"subtitle": "Eingebettetes Trust Wallet (Browser-Erweiterung oder In-App dApp Browser).",
"subtitle": "Eingebettete Wallet (Trust Wallet- oder Phantom-Erweiterung).",
"pill": "Nur Polygon",
"connect": "Trust Wallet verbinden",
"connectWallet": "{wallet} verbinden",
"disconnect": "Trennen",
"account": "Konto",
"chain": "Netzwerk",
@@ -59,6 +60,12 @@
"footer": {
"repo": "Quellcode auf Gitea"
},
"wallet": {
"pick": "Wallet wählen",
"helper": "Nur injizierte Erweiterungen. Wähle Trust Wallet oder Phantom und verbinde dich.",
"trust": "Trust Wallet",
"phantom": "Phantom"
},
"status": {
"waiting": "Warten auf Verbindung…",
"preparingConnect": "Verbindung wird vorbereitet…",
@@ -78,7 +85,7 @@
"disconnected": "Getrennt."
},
"errors": {
"noWallet": "Keine eingebettete Wallet gefunden. Verwende die Trust-Wallet-Erweiterung oder den In-App-Browser.",
"noWallet": "Keine injizierte {wallet} gefunden. Installiere die Erweiterung und versuche es erneut.",
"noAccounts": "Die Wallet hat keine Konten zurückgegeben.",
"sendFailed": "Nachrichten-Transaktion konnte nicht gesendet werden.",
"connectionFailed": "Verbindung fehlgeschlagen."

View File

@@ -9,15 +9,16 @@
"light": "Light mode"
},
"hero": {
"eyebrow": "Injected Trust Wallet • Polygon mainnet",
"title": "PolyNote · Trust Wallet to Polygon",
"lede": "Connect with the Trust Wallet extension or the in-app browser. We hex-encode your text and submit a zero-value Polygon transaction that only costs gas."
"eyebrow": "Trust Wallet or Phantom • Polygon mainnet",
"title": "PolyNote · Injected wallets to Polygon",
"lede": "Connect with the Trust Wallet or Phantom browser extensions. We hex-encode your text and submit a zero-value Polygon transaction that only costs gas."
},
"connection": {
"title": "Connection",
"subtitle": "Injected Trust Wallet (browser extension or in-app dApp browser).",
"subtitle": "Injected wallet (Trust Wallet or Phantom extension).",
"pill": "Polygon only",
"connect": "Connect Trust Wallet",
"connectWallet": "Connect {wallet}",
"disconnect": "Disconnect",
"account": "Account",
"chain": "Chain",
@@ -59,6 +60,12 @@
"footer": {
"repo": "Source on Gitea"
},
"wallet": {
"pick": "Choose a wallet",
"helper": "Injected extensions only. Pick Trust Wallet or Phantom, then connect.",
"trust": "Trust Wallet",
"phantom": "Phantom"
},
"status": {
"waiting": "Waiting to connect…",
"preparingConnect": "Preparing to connect…",
@@ -78,7 +85,7 @@
"disconnected": "Disconnected."
},
"errors": {
"noWallet": "No injected wallet found. Try the Trust Wallet extension or app browser.",
"noWallet": "No injected {wallet} found. Install the extension and try again.",
"noAccounts": "No accounts returned from injected wallet.",
"sendFailed": "Failed to send message transaction.",
"connectionFailed": "Connection failed."

View File

@@ -9,15 +9,16 @@
"light": "Modo claro"
},
"hero": {
"eyebrow": "Trust Wallet inyectado • Polygon mainnet",
"title": "PolyNote · Trust Wallet a Polygon",
"lede": "Conecta con la extensión de Trust Wallet o el navegador dentro de la app. Codificamos tu texto en hex y enviamos una transacción de valor cero en Polygon; solo pagas gas."
"eyebrow": "Trust Wallet o Phantom • Polygon mainnet",
"title": "PolyNote · Wallets inyectadas a Polygon",
"lede": "Conecta con las extensiones de Trust Wallet o Phantom. Codificamos tu texto en hex y enviamos una transacción de valor cero en Polygon; solo pagas gas."
},
"connection": {
"title": "Conexión",
"subtitle": "Trust Wallet inyectado (extensión o navegador dentro de la app).",
"subtitle": "Wallet inyectada (extensión Trust Wallet o Phantom).",
"pill": "Solo Polygon",
"connect": "Conectar Trust Wallet",
"connectWallet": "Conectar {wallet}",
"disconnect": "Desconectar",
"account": "Cuenta",
"chain": "Red",
@@ -59,6 +60,12 @@
"footer": {
"repo": "Código fuente en Gitea"
},
"wallet": {
"pick": "Elige una wallet",
"helper": "Solo extensiones inyectadas. Elige Trust Wallet o Phantom y conecta.",
"trust": "Trust Wallet",
"phantom": "Phantom"
},
"status": {
"waiting": "Esperando conexión…",
"preparingConnect": "Preparando conexión…",
@@ -78,7 +85,7 @@
"disconnected": "Desconectado."
},
"errors": {
"noWallet": "No se encontró una wallet inyectada. Usa la extensión de Trust Wallet o el navegador de la app.",
"noWallet": "No se encontró una {wallet} inyectada. Instala la extensión e inténtalo de nuevo.",
"noAccounts": "La wallet no devolvió cuentas.",
"sendFailed": "No se pudo enviar la transacción con mensaje.",
"connectionFailed": "Fallo de conexión."

View File

@@ -9,15 +9,16 @@
"light": "Mode clair"
},
"hero": {
"eyebrow": "Trust Wallet injecté • Polygon mainnet",
"title": "PolyNote · Trust Wallet vers Polygon",
"lede": "Connecte-toi avec lextension Trust Wallet ou le navigateur intégré. Nous encodons ton texte en hex et envoyons une transaction à 0 MATIC sur Polygon ; seuls les frais de gas sont dus."
"eyebrow": "Trust Wallet ou Phantom • Polygon mainnet",
"title": "PolyNote · Wallets injectés vers Polygon",
"lede": "Connecte-toi avec les extensions Trust Wallet ou Phantom. Nous encodons ton texte en hex et envoyons une transaction à 0 MATIC sur Polygon ; seuls les frais de gas sont dus."
},
"connection": {
"title": "Connexion",
"subtitle": "Trust Wallet injecté (extension navigateur ou navigateur dApp intégré).",
"subtitle": "Wallet injectée (extension Trust Wallet ou Phantom).",
"pill": "Polygon uniquement",
"connect": "Connecter Trust Wallet",
"connectWallet": "Connecter {wallet}",
"disconnect": "Déconnecter",
"account": "Compte",
"chain": "Réseau",
@@ -59,6 +60,12 @@
"footer": {
"repo": "Code source sur Gitea"
},
"wallet": {
"pick": "Choisis un wallet",
"helper": "Extensions injectées uniquement. Choisis Trust Wallet ou Phantom, puis connecte-toi.",
"trust": "Trust Wallet",
"phantom": "Phantom"
},
"status": {
"waiting": "En attente de connexion…",
"preparingConnect": "Préparation de la connexion…",
@@ -78,7 +85,7 @@
"disconnected": "Déconnecté."
},
"errors": {
"noWallet": "Aucune wallet injectée trouvée. Utilise lextension Trust Wallet ou le navigateur de lapp.",
"noWallet": "Aucun {wallet} injecté trouvé. Installe lextension et réessaie.",
"noAccounts": "La wallet na renvoyé aucun compte.",
"sendFailed": "Impossible denvoyer la transaction message.",
"connectionFailed": "Échec de connexion."

View File

@@ -9,15 +9,16 @@
"light": "Modalità chiara"
},
"hero": {
"eyebrow": "Trust Wallet iniettato • Polygon mainnet",
"title": "PolyNote · Trust Wallet su Polygon",
"lede": "Connetti con lestensione di Trust Wallet o il browser integrato. Codifichiamo il testo in hex e inviamo una transazione a 0 MATIC su Polygon; paghi solo il gas."
"eyebrow": "Trust Wallet o Phantom • Polygon mainnet",
"title": "PolyNote · Wallet iniettate su Polygon",
"lede": "Connettiti con le estensioni di Trust Wallet o Phantom. Codifichiamo il testo in hex e inviamo una transazione a 0 MATIC su Polygon; paghi solo il gas."
},
"connection": {
"title": "Connessione",
"subtitle": "Trust Wallet iniettato (estensione o browser dApp integrato).",
"subtitle": "Wallet iniettata (estensione Trust Wallet o Phantom).",
"pill": "Solo Polygon",
"connect": "Connetti Trust Wallet",
"connectWallet": "Connetti {wallet}",
"disconnect": "Disconnetti",
"account": "Account",
"chain": "Rete",
@@ -59,6 +60,12 @@
"footer": {
"repo": "Codice sorgente su Gitea"
},
"wallet": {
"pick": "Scegli un wallet",
"helper": "Solo estensioni iniettate. Scegli Trust Wallet o Phantom, poi connettiti.",
"trust": "Trust Wallet",
"phantom": "Phantom"
},
"status": {
"waiting": "In attesa di connessione…",
"preparingConnect": "Preparazione della connessione…",
@@ -78,7 +85,7 @@
"disconnected": "Disconnesso."
},
"errors": {
"noWallet": "Nessun wallet iniettato trovato. Usa lestensione Trust Wallet o il browser dellapp.",
"noWallet": "Nessuna {wallet} iniettata trovata. Installa lestensione e riprova.",
"noAccounts": "Il wallet non ha restituito account.",
"sendFailed": "Impossibile inviare la transazione con messaggio.",
"connectionFailed": "Connessione fallita."

View File

@@ -9,15 +9,16 @@
"light": "Ljust läge"
},
"hero": {
"eyebrow": "Injektad Trust Wallet • Polygon mainnet",
"title": "PolyNote · Trust Wallet till Polygon",
"lede": "Anslut via Trust Wallets tillägg eller den inbyggda webbläsaren. Vi kodar din text i hex och skickar en 0 MATIC-transaktion på Polygon; du betalar bara gas."
"eyebrow": "Trust Wallet eller Phantom • Polygon mainnet",
"title": "PolyNote · Injekterade wallets till Polygon",
"lede": "Anslut med Trust Wallet- eller Phantom-tilläggen. Vi kodar din text i hex och skickar en 0 MATIC-transaktion på Polygon; du betalar bara gas."
},
"connection": {
"title": "Anslutning",
"subtitle": "Injekterad Trust Wallet (tillägg eller in-app dApp-webbläsare).",
"subtitle": "Injekterad wallet (Trust Wallet- eller Phantom-tillägg).",
"pill": "Endast Polygon",
"connect": "Anslut Trust Wallet",
"connectWallet": "Anslut {wallet}",
"disconnect": "Koppla från",
"account": "Konto",
"chain": "Nätverk",
@@ -59,6 +60,12 @@
"footer": {
"repo": "Källkod på Gitea"
},
"wallet": {
"pick": "Välj en wallet",
"helper": "Endast injekterade tillägg. Välj Trust Wallet eller Phantom och anslut.",
"trust": "Trust Wallet",
"phantom": "Phantom"
},
"status": {
"waiting": "Väntar på anslutning…",
"preparingConnect": "Förbereder anslutning…",
@@ -78,7 +85,7 @@
"disconnected": "Frånkopplad."
},
"errors": {
"noWallet": "Ingen injekterad plånbok hittades. Använd Trust Wallet-tillägget eller appens webbläsare.",
"noWallet": "Ingen injekterad {wallet} hittades. Installera tillägget och försök igen.",
"noAccounts": "Plånboken returnerade inga konton.",
"sendFailed": "Det gick inte att skicka meddelande-transaktionen.",
"connectionFailed": "Anslutningen misslyckades."

View File

@@ -24,11 +24,13 @@ type Eip1193Provider = {
providers?: Eip1193Provider[];
isTrustWallet?: boolean;
isTrust?: boolean;
isPhantom?: boolean;
};
type EthereumWindow = Window & {
ethereum?: Eip1193Provider & { providers?: Eip1193Provider[] };
trustwallet?: Eip1193Provider;
phantom?: { ethereum?: Eip1193Provider };
};
const formatChain = (chainId: number | null) => {
@@ -45,6 +47,32 @@ type MessageEntry = {
createdAt: number;
};
function TrustIcon() {
return (
<svg className="wallet-icon" viewBox="0 0 64 64" aria-hidden="true">
<path
fill="#2b5dff"
d="M32 6c-5.7 4.8-12.1 7-18.4 7.4l.7 21c.3 9.5 6.3 18 17.7 23.6 11.4-5.6 17.4-14 17.7-23.6l.7-21C44.1 13 37.7 10.8 32 6Z"
/>
<path
fill="#fff"
d="M32 11.6c5 3.6 10.3 5.4 15.6 5.9l-.6 17.8c-.3 8.1-5.1 14.9-15 19.7-9.9-4.8-14.8-11.6-15-19.7l-.6-17.8c5.3-.5 10.6-2.3 15.6-5.9Z"
/>
</svg>
);
}
function PhantomIcon() {
return (
<svg className="wallet-icon" viewBox="0 0 64 64" aria-hidden="true">
<path
fill="#6c3bff"
d="M54.5 25.7C52 13.9 42.2 8 33.8 8 24.1 8 12 14.5 8 29.7 5.8 37.7 10.9 41.4 14.4 41.4c4 0 7.9-2.4 10.1-5.6.5 4.1 3.9 7.5 8.1 7.5 4.7 0 8.6-4 8.6-9 0-1.5-.3-2.9-.8-4.2 0 0 6.5.5 8.3 7.1 1.1 4.2 2 7 4 7 1.7 0 3.7-2.4 1.8-18.5ZM15.5 33c-1.4 0-2.5-1.5-2.5-3.3s1.1-3.3 2.5-3.3c1.4 0 2.5 1.5 2.5 3.3S16.9 33 15.5 33Zm11 0c-1.4 0-2.5-1.5-2.5-3.3s1.1-3.3 2.5-3.3c1.4 0 2.5 1.5 2.5 3.3S27.9 33 26.5 33Z"
/>
</svg>
);
}
export default function HomePage() {
const { locale, setLocale, t, localeOptions } = useI18n("en");
const [account, setAccount] = useState<string>("");
@@ -55,6 +83,7 @@ export default function HomePage() {
key: "status.waiting",
tone: "info",
});
const [walletChoice, setWalletChoice] = useState<"trust" | "phantom">("trust");
const [connecting, setConnecting] = useState<boolean>(false);
const [sending, setSending] = useState<boolean>(false);
const [connected, setConnected] = useState<boolean>(false);
@@ -72,6 +101,7 @@ export default function HomePage() {
: status.tone === "error"
? "status-line status-error"
: "status-line";
const walletLabel = walletChoice === "trust" ? t("wallet.trust") : t("wallet.phantom");
const chainPillClass = onPolygon ? "pill pill-live" : "pill pill-quiet";
const chainPillText = onPolygon ? "Polygon" : chainLabel || t("live.chainRequired");
const disableSend = !connected || sending;
@@ -106,7 +136,7 @@ export default function HomePage() {
await disconnect(false);
try {
await connectInjected();
await connectInjected(walletChoice);
} catch (err) {
console.error("Connection failed", err);
setStatusMessage(normalizeError(err, t("errors.connectionFailed"), t), "error");
@@ -116,10 +146,14 @@ export default function HomePage() {
}
}
async function connectInjected() {
const injected = pickInjectedProvider();
async function connectInjected(choice: "trust" | "phantom") {
const injected = pickInjectedProvider(choice);
if (!injected) {
setStatusMessage(t("errors.noWallet"), "error", "errors.noWallet");
setStatusMessage(
t("errors.noWallet", { wallet: t(choice === "trust" ? "wallet.trust" : "wallet.phantom") }),
"error",
"errors.noWallet"
);
return;
}
@@ -174,6 +208,23 @@ export default function HomePage() {
setTxHash("—");
}
async function refreshSigner() {
if (!providerRef.current) return;
try {
const browserProvider = new ethers.BrowserProvider(
providerRef.current as unknown as ethers.Eip1193Provider
);
const signer = await browserProvider.getSigner();
signerRef.current = signer;
const address = await signer.getAddress();
const network = await browserProvider.getNetwork();
setAccount(address);
setChainId(Number(network.chainId));
} catch (err) {
console.warn("Failed to refresh signer after chain change", err);
}
}
function attachProviderEvents(provider: Eip1193Provider) {
if (!provider?.on) return;
provider.on("accountsChanged", handleAccountsChanged);
@@ -210,6 +261,7 @@ export default function HomePage() {
if (typeof newChainId !== "string" && typeof newChainId !== "number") return;
const parsed = parseChainId(newChainId);
setChainId(parsed);
void refreshSigner();
}
async function ensurePolygonChain() {
@@ -220,6 +272,7 @@ export default function HomePage() {
params: [{ chainId: "0x89" }],
});
setChainId(137);
await refreshSigner();
return true;
} catch (err) {
console.warn("Chain switch rejected", err);
@@ -393,6 +446,33 @@ export default function HomePage() {
{theme === "dark" ? t("theme.light") : t("theme.dark")}
</button>
</div>
<div className="wallet-selector">
<div className="wallet-selector-head">
<p className="label">{t("wallet.pick")}</p>
<p className="muted">{t("wallet.helper")}</p>
</div>
<div className="wallet-toggle">
<button
type="button"
className={`wallet-chip ${walletChoice === "trust" ? "active" : ""}`}
onClick={() => setWalletChoice("trust")}
aria-pressed={walletChoice === "trust"}
>
<TrustIcon />
{t("wallet.trust")}
</button>
<button
type="button"
className={`wallet-chip ${walletChoice === "phantom" ? "active" : ""}`}
onClick={() => setWalletChoice("phantom")}
aria-pressed={walletChoice === "phantom"}
>
<PhantomIcon />
{t("wallet.phantom")}
</button>
</div>
</div>
</div>
</header>
@@ -413,7 +493,9 @@ export default function HomePage() {
onClick={connectWallet}
disabled={disableConnect}
>
{connecting ? t("compose.sending") : t("connection.connect")}
{connecting
? t("compose.sending")
: t("connection.connectWallet", { wallet: walletLabel })}
</button>
<button
id="disconnectBtn"
@@ -566,7 +648,7 @@ export default function HomePage() {
<footer className="footer">
<div className="footer-left">
<span className="version-pill">1</span>
<span className="version-pill">2</span>
</div>
<a
className="footer-link"
@@ -589,16 +671,33 @@ export default function HomePage() {
);
}
function pickInjectedProvider(): Eip1193Provider | null {
function pickInjectedProvider(choice: "trust" | "phantom"): Eip1193Provider | null {
if (typeof window === "undefined") return null;
const { ethereum, trustwallet } = window as EthereumWindow;
if (ethereum?.providers?.length) {
const trust = ethereum.providers.find((p) => p.isTrustWallet || p.isTrust);
const { ethereum, trustwallet, phantom } = window as EthereumWindow;
const providers = ethereum?.providers || [];
const isTrustProvider = (p: Eip1193Provider | null | undefined) =>
Boolean(p?.isTrustWallet || p?.isTrust);
const isPhantomProvider = (p: Eip1193Provider | null | undefined) =>
Boolean(p?.isPhantom || (phantom?.ethereum && p === phantom.ethereum));
if (choice === "trust") {
const trust = providers.find(isTrustProvider);
if (trust) return trust;
return ethereum.providers[0];
}
if (trustwallet) return trustwallet;
if (providers.length) return providers[0];
return ethereum || null;
}
if (choice === "phantom") {
const phantomFromArray = providers.find(isPhantomProvider);
if (phantomFromArray) return phantomFromArray;
if (phantom?.ethereum) return phantom.ethereum;
if (ethereum?.isPhantom) return ethereum;
return ethereum || null;
}
return null;
}
function parseChainId(chainId: string | number): number {

View File

@@ -1,6 +1,6 @@
{
"name": "poly-note",
"version": "1.0.0",
"version": "2.0.0",
"type": "module",
"scripts": {
"dev": "node scripts/ensure-middleware-manifest.cjs && pnpm exec next dev",

1
public/phantom.svg Normal file
View File

@@ -0,0 +1 @@
404: Not Found

BIN
public/trustwallet.svg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB