Menandatangani pesan membuktikan bahwa pengguna mengendalikan sebuah dompet, tanpa transaksi, tanpa biaya, dan tanpa panggilan jaringan. Aplikasi menggunakan pesan yang ditandatangani untuk memasukkan pengguna (sign in), mencatat bahwa pengguna telah menerima ketentuan, menautkan dompet ke akun yang sudah ada, atau mengotorisasi tindakan yang terjadi di luar chain (off-chain), seperti membuat pesanan.
Dompet Solana menandatangani pesan dengan dua cara:
- Pesan off-chain (
solana:signOffchainMessage) membungkus teks Anda dalam format standar yang didefinisikan oleh spesifikasi penandatanganan pesan off-chain (OCMS). Format ini tidak akan pernah bisa disalahartikan sebagai transaksi, menyebutkan akun yang harus menandatanganinya, dan merupakan format yang ditandatangani oleh dompet perangkat keras. - Pesan mentah (raw) (
solana:signMessage) menandatangani persis byte yang diteruskan aplikasi Anda, tanpa awalan dan tanpa penanda tangan yang dideklarasikan. Sebagian besar dompet perangkat lunak mendukungnya, tetapi tidak ada apa pun pada byte tersebut yang menandainya sebagai pesan, bukan transaksi.
Gunakan pesan off-chain jika dompet mendukungnya, dan beralih ke pesan mentah jika tidak. Sisa panduan ini menggunakan sign-in sebagai contoh kasus, tetapi langkah penandatanganan dan verifikasi yang sama berlaku untuk pesan apa pun.
Pilih format pesan
| Pesan off-chain | Pesan mentah | Transaksi | |
|---|---|---|---|
| Fitur dompet | signOffchainMessage | signMessage | signTransaction |
| Dukungan dompet perangkat keras | Tergantung perangkat | Tidak | Ya |
| Dapat dibaca pengguna | Ya | Hanya perangkat lunak | Tergantung perangkat |
| Mendeklarasikan penanda tangan yang diperlukan | Ya | Tidak | Ya |
| Panggilan RPC dan biaya | Tidak ada | Tidak ada | Blockhash, kirim, biaya, konfirmasi |
Mengapa bukan transaksi?
Dulu dompet perangkat keras tidak punya cara untuk menandatangani pesan yang dapat dibaca. Banyak aplikasi terpaksa meminta pengguna ini menandatangani transaksi no-op. Cara ini berhasil, tetapi menambah latensi, biaya, dan risiko:
- Biaya jaringan. Transaksi membayar biaya, bahkan ketika tidak melakukan apa pun.
- Banyak panggilan RPC. Aplikasi Anda mengambil blockhash, melakukan simulasi, mengirim, dan kemudian melakukan polling untuk konfirmasi.
- Latensi tambahan. Setiap panggilan tersebut adalah satu putaran bolak-balik sebelum pengguna berhasil masuk.
- Tidak ada yang dapat dibaca. Pengguna menyetujui byte transaksi, bukan pernyataan tentang apa yang mereka setujui.
- Risiko perilaku. Meminta pengguna menandatangani transaksi hanya untuk login membuat mereka lebih mudah ditipu untuk menandatangani transaksi yang berbahaya.
Pesan off-chain melewati semua itu. Satu-satunya putaran bolak-balik hanya ke server Anda sendiri.
Apa yang ditandatangani dompet
Pesan off-chain diawali dengan domain penandatanganan tetap, \xffsolana offchain.
Tidak ada transaksi valid yang diawali dengan byte tersebut, sehingga pesan yang ditandatangani tidak akan pernah bisa
digunakan sebagai transaksi. Format versi 1 kemudian mencantumkan penanda tangan yang diperlukan
dan membawa pesan sebagai teks UTF-8, tanpa batas panjang.
Dompet membuat byte ini, menandatanganinya, dan mengembalikannya bersama tanda tangan. Spesifikasi membahas format lengkapnya, termasuk versi 0 yang lama.
Pilih metode sign-in
Belum semua dompet mendukung pesan off-chain. Periksa fitur akun yang terhubung sebelum meminta tanda tangan, dan gunakan cadangan jika fitur tersebut tidak tersedia:
- Jika akun mendukung
solana:signOffchainMessagedengan pesan versi 1, tandatangani pesan off-chain. - Jika tidak, tandatangani challenge yang sama dengan
solana:signMessage. Sebagian besar dompet perangkat lunak mendukungnya. - Berikan pengguna dompet perangkat keras tanpa dukungan pesan opsi opt-in yang eksplisit, seperti tombol "Saya menggunakan dompet perangkat keras", yang menandatangani transaksi sign-in sebagai gantinya. Jangan arahkan mereka ke jalur ini secara default.
Menandatangani pesan di browser
Instal paket Wallet Standard bersama Kit:
$npm install @solana/kit @solana/wallet-standard-features @wallet-standard/ui @wallet-standard/ui-features @wallet-standard/ui-registry
Helper ini menerima UiWalletAccount, seperti account yang dikembalikan oleh
useConnectedWallet di panduan React:
import {SolanaSignOffchainMessage,type SolanaSignOffchainMessageFeature} from "@solana/wallet-standard-features";import type { UiWalletAccount } from "@wallet-standard/ui";import { getWalletAccountFeature } from "@wallet-standard/ui-features";import { getWalletAccountForUiWalletAccount } from "@wallet-standard/ui-registry";type OffchainMessageFeature =SolanaSignOffchainMessageFeature[typeof SolanaSignOffchainMessage];export function supportsOffchainMessages(account: UiWalletAccount) {if (!account.features.includes(SolanaSignOffchainMessage)) return false;const feature = getWalletAccountFeature(account,SolanaSignOffchainMessage) as OffchainMessageFeature;return feature.supportedMessageVersions.includes(1);}export async function signOffchainMessage(account: UiWalletAccount,message: string) {const feature = getWalletAccountFeature(account,SolanaSignOffchainMessage) as OffchainMessageFeature;const [output] = await feature.signOffchainMessage({messageVersion: 1,account: getWalletAccountForUiWalletAccount(account),message,requiredSigners: [account.publicKey]});return output;}
signOffchainMessage mengembalikan signedOffchainMessage, yaitu byte lengkap yang ditandatangani
dompet, dan signature. Encode keduanya dengan Base64 dan kirim ke server Anda bersama
alamat dompet.
Tulis challenge agar mudah dibaca di layar kecil. Sebutkan nama aplikasi dan domain Anda, jelaskan apa yang dilakukan penandatanganan, dan sertakan nonce sekali pakai serta waktu penerbitan dari server Anda:
swap.app wants you to sign in with your Solana account.Signing in costs nothing and does not submit a transaction.Nonce: 8f3a1c9eIssued at: 2026-09-29T17:04:00Z
Verifikasi tanda tangan di server Anda
Verifikasi merupakan pemeriksaan tanda tangan secara lokal dan tidak memerlukan koneksi RPC. Decode byte yang ditandatangani dompet, pastikan byte tersebut cocok dengan challenge yang Anda terbitkan, lalu periksa tanda tangannya:
import {address,assertOffchainMessageV1Equal,getBase64Encoder,getOffchainMessageDecoder,verifyOffchainMessageEnvelope,type OffchainMessageBytes,type SignatureBytes} from "@solana/kit";export async function verifySignIn(input: {walletAddress: string;expectedMessage: string;signedOffchainMessage: string;signature: string;}) {const signer = address(input.walletAddress);const base64 = getBase64Encoder();const content = base64.encode(input.signedOffchainMessage) as OffchainMessageBytes;const signature = base64.encode(input.signature) as SignatureBytes;const received = getOffchainMessageDecoder().decode(content);if (received.version !== 1) {throw new Error(`Expected a v1 message, got v${received.version}`);}assertOffchainMessageV1Equal(received, {version: 1,requiredSignatories: [{ address: signer }],content: input.expectedMessage});await verifyOffchainMessageEnvelope({content,signatures: { [signer]: signature }});}
assertOffchainMessageV1Equal melempar error jika konten atau penanda tangan berbeda dari
yang Anda harapkan. verifyOffchainMessageEnvelope melempar error jika tanda tangan
tidak ada atau tidak valid. Cari expectedMessage berdasarkan nonce di server Anda, bukan
mempercayai salinan yang dikirim kembali oleh klien. Gunakan (consume) nonce setelah
verifikasi berhasil, sehingga tanda tangan yang sama tidak dapat dipakai untuk sign-in dua kali.
Memverifikasi pesan mentah
Tanda tangan signMessage mencakup byte challenge secara langsung, sehingga klien
hanya perlu mengirim tanda tangan dan alamat dompet. Periksa terhadap
challenge yang Anda terbitkan:
import {address,getBase64Encoder,getPublicKeyFromAddress,getUtf8Encoder,signatureBytes,verifySignature} from "@solana/kit";export async function verifyRawSignIn(input: {walletAddress: string;expectedMessage: string;signature: string;}) {const publicKey = await getPublicKeyFromAddress(address(input.walletAddress));const signature = signatureBytes(getBase64Encoder().encode(input.signature));const message = getUtf8Encoder().encode(input.expectedMessage);if (!(await verifySignature(publicKey, signature, message))) {throw new Error("Invalid signature");}}
Aturan nonce yang sama berlaku: cari expectedMessage berdasarkan nonce dan gunakan (consume) nonce
setelah verifikasi berhasil.
@solana/kit mengekspor ulang helper ini dari @solana/offchain-messages. Gunakan
paket mandiri jika server Anda tidak bergantung pada Kit.
Sign In With Solana
Sign In With Solana (SIWS)
menstandardisasi challenge itu sendiri. Aplikasi Anda meneruskan kolom terstruktur, seperti
domain, statement, nonce, dan issuedAt, ke fitur solana:signIn milik dompet.
Dompet memformatnya menjadi pesan standar, terhubung jika diperlukan,
dan menandatanganinya dalam satu langkah.
Versi 1.1.0 dari solana:signIn menambahkan opsi useOffchainMessage. Ketika Anda
meneruskan useOffchainMessage: { messageVersion: 1 }, dompet membungkus teks SIWS
dalam pesan off-chain versi 1 dengan akun penanda tangan sebagai satu-satunya penanda tangan
yang diperlukan. Dompet perangkat keras dapat menampilkan dan menandatanganinya, dan output melaporkan
signedMessageFormat: { kind: "offchainMessage", messageVersion: 1 }. Dompet
yang mengiklankan versi 1.0.0 tidak mengenal opsi ini, jadi periksa versi fitur
terlebih dahulu dan abaikan opsi tersebut jika tidak didukung.
solana:signIn adalah fitur dompet, bukan fitur akun, sehingga helper
ini menerima UiWallet:
import {SolanaSignIn,type SolanaSignInFeature,type SolanaSignInInput} from "@solana/wallet-standard-features";import type { UiWallet } from "@wallet-standard/ui";import { getWalletFeature } from "@wallet-standard/ui-features";type SignInFeature = SolanaSignInFeature[typeof SolanaSignIn];export function supportsOffchainSignIn(wallet: UiWallet) {if (!wallet.features.includes(SolanaSignIn)) return false;const feature = getWalletFeature(wallet, SolanaSignIn) as SignInFeature;return feature.version === "1.1.0";}export async function signIn(wallet: UiWallet, input: SolanaSignInInput) {const feature = getWalletFeature(wallet, SolanaSignIn) as SignInFeature;const [output] = await feature.signIn({...input,...(supportsOffchainSignIn(wallet) && {useOffchainMessage: { messageVersion: 1 }})});return output;}
Susun input di server Anda, dengan nonce sekali pakai dan waktu issuedAt,
lalu simpan salinannya dengan nonce sebagai kunci. Gunakan (consume) nonce setelah sign-in
berhasil diverifikasi.
Tanpa Wallet Adapter, verifySignIn dari @solana/wallet-standard-util
hanya memeriksa output SIWS biasa. Ketika signedMessageFormat melaporkan pesan
off-chain, signedMessage adalah pesan off-chain yang kontennya adalah teks SIWS.
Bangun ulang teks yang diharapkan dari input yang Anda simpan dan alamat penanda tangan dengan
createSignInMessageText dari @solana/wallet-standard-util, lalu verifikasi
seperti pesan off-chain lainnya, seperti pada
Verifikasi tanda tangan di server Anda. README @solana/offchain-messages
membahas alur ini di Kit.
Untuk dompet
Dompet menambahkan dukungan dengan mengimplementasikan fitur Wallet Standard
solana:signOffchainMessage dari @solana/wallet-standard-features:
- Laporkan
version: "1.0.0"dan cantumkan versi pesan yang dapat Anda tandatangani disupportedMessageVersions. Saat ini, nilainya adalah[1]. - Buat preamble versi 1 dari
requiredSigners, yang Anda urutkan dan periksa apakah ada duplikat. Tolak permintaan jika akun bukan salah satu penandatangan yang diwajibkan. - Tampilkan teks pesan kepada pengguna secara utuh sebelum penandatanganan, termasuk di perangkat keras.
- Kembalikan byte persis yang Anda tandatangani sebagai
signedOffchainMessage, besertasignatureEd25519. - Untuk mendukung Sign In With Solana secara off-chain, laporkan
version: "1.1.0"untuksolana:signIn. Jika input menyertakanuseOffchainMessage, tandatangani teks SIWS sebagai pesan off-chain versi 1, kembalikan byte-nya sebagaisignedMessage, dan atursignedMessageFormat. - Teruslah mendukung
solana:signMessage. Fungsi ini tidak berubah, dan aplikasi tetap menggunakannya sebagai cadangan.
Is this page helpful?