@vue-solana/vue

Solana 앱을 위한 Vue 플러그인과 컴포저블입니다.

@vue-solana/vue는 Solana RPC 접근, 잔액 읽기, 지갑 상태, 트랜잭션 helper 상태를 위한 Vue 플러그인과 컴포저블을 제공합니다.

설치

pnpm add @vue-solana/vue

트랜잭션을 만들거나 직렬화하는 브라우저 앱은 @vue-solana/vue/buffer-polyfill에서 Buffer polyfill을 초기화할 수 있습니다.

플러그인 설정

import { createApp } from "vue";
import { createSolanaPlugin } from "@vue-solana/vue";
import App from "./App.vue";

createApp(App)
  .use(
    createSolanaPlugin({
      cluster: "devnet",
      mobileWallet: {
        appIdentity: {
          name: "My Vue Solana App",
          uri: "https://example.com",
          icon: "favicon.ico",
        },
      },
    }),
  )
  .mount("#app");

Android Mobile Wallet Adapter 등록은 지원되는 Android Chrome 클라이언트에서 기본으로 활성화됩니다. MWA app identity를 조정하려면 mobileWallet 옵션을 전달하고, Android mobile wallet 등록을 비활성화하려면 mobileWallet: false를 전달하세요.

iOS browser wallet link는 iOS 브라우저에서 Phantom, Solflare, Backpack에 대해 기본으로 활성화됩니다. App identity, redirect URL, chains, cluster를 조정하려면 iosWallet 옵션을 전달하고, iOS wallet link discovery를 비활성화하려면 iosWallet: false를 전달하세요.

커스텀 RPC 엔드포인트도 전달할 수 있습니다.

createApp(App).use(
  createSolanaPlugin({
    cluster: "mainnet-beta",
    endpoint: "https://your-rpc.example.com",
    commitment: "confirmed",
  }),
);

컴포저블

Root export는 계속 지원됩니다. 컴포저블은 새 코드에서 direct subpath import를 선호하세요. 이렇게 하면 bundler가 관련 없는 package entry 코드를 평가하지 않아도 됩니다.

import { useRpc } from "@vue-solana/vue/useRpc";
import { useWallet } from "@vue-solana/vue/useWallet";

Direct package subpath:

  • @vue-solana/vue/buffer-polyfill
  • @vue-solana/vue/useSolana
  • @vue-solana/vue/useRpc
  • @vue-solana/vue/useConnection
  • @vue-solana/vue/useAccountInfo
  • @vue-solana/vue/useBalance
  • @vue-solana/vue/useProgramAccounts
  • @vue-solana/vue/useWallet
  • @vue-solana/vue/useWallets
  • @vue-solana/vue/useTransaction
  • @vue-solana/vue/useTransactionConfirmation
  • @vue-solana/vue/useSignatureStatus
  • @vue-solana/vue/useSignMessage
  • @vue-solana/vue/useSignAndSendTransaction
  • @vue-solana/vue/web3

PublicKey, Transaction, TransactionInstruction 같은 지원되는 raw Solana primitive에는 @vue-solana/vue/web3를 사용하세요. Buffer polyfill이 필요한 브라우저 트랜잭션 코드에는 @vue-solana/vue/buffer-polyfill을 사용하세요. 더 낮은 수준의 core 사용에는 direct @vue-solana/core/* import도 계속 지원됩니다.

  • useSolana(): 주입된 전체 Solana context를 반환합니다.
  • useRpc(): cluster, endpoint, connection status, latest blockhash, checkConnection()을 반환합니다.
  • useConnection(): Solana Connection을 반환합니다.
  • useAccountInfo(address, options?): account data를 로드하고 account 변경을 subscribe할 수 있습니다.
  • useProgramAccounts(programId, options?): optional filters와 data slicing으로 program id가 소유한 accounts를 로드합니다.
  • useWallet(): active wallet ref, computed connection state, wallet action을 반환합니다.
  • useWallets(): 발견된 browser extension wallet, Android Mobile Wallet Adapter wallet, 지원되는 iOS browser wallet entry, wallet 선택 action을 반환합니다.
  • useBalance(address, commitment?): PublicKey 또는 address string의 lamport balance를 로드합니다.
  • useTransaction(handler, options?): optional timeout 설정을 지원하는 generic async transaction state helper입니다.
  • useTransactionConfirmation(options?): 제출된 signature를 reactive status 및 timeout/error state와 함께 confirm합니다.
  • useSignatureStatus(signature, options?): signature status update를 읽거나 polling하거나 subscribe합니다.
  • useSignMessage(): 지원되는 경우 설정된 wallet을 통해 임의의 인증 메시지에 서명합니다.
  • useSignAndSendTransaction(): optional confirmation waiting과 함께 설정된 wallet을 통해 transaction에 서명하고 전송합니다.

관련 가이드

  • RPC와 클러스터: connection state를 읽고 endpoint를 설정합니다.
  • 지갑: wallet을 discover, select, connect, disconnect하고 capability를 확인합니다.
  • 계정 읽기: balance, account info, program accounts, signature status를 읽습니다.
  • 트랜잭션: transaction을 sign, send, confirm하고 진행 상태를 표시합니다.
  • 메시지 서명: 오프체인 인증 또는 소유권 challenge에 서명합니다.
  • 오류: composable error ref를 안전한 UI 메시지로 매핑합니다.

RPC 상태 읽기

<script setup lang="ts">
import { computed } from "vue";
import { useRpc } from "@vue-solana/vue/useRpc";

const { cluster, endpoint, status, error, latestBlockhash, checkConnection } = useRpc();

const rpcErrorMessage = computed(() => {
  if (!error.value) return null;
  return error.value.code === "RPC_FAILURE"
    ? "Unable to reach the configured Solana RPC endpoint."
    : "Unable to check the Solana connection.";
});
</script>

<template>
  <section>
    <p>Cluster: {{ cluster }}</p>
    <p>Endpoint: {{ endpoint }}</p>
    <p>Status: {{ status }}</p>
    <p>Latest blockhash: {{ latestBlockhash }}</p>
    <p v-if="rpcErrorMessage">{{ rpcErrorMessage }}</p>
    <button type="button" @click="checkConnection">Check RPC</button>
  </section>
</template>

잔액 읽기

<script setup lang="ts">
import { computed, ref } from "vue";
import { useBalance } from "@vue-solana/vue/useBalance";

const address = ref("PASTE_A_SOLANA_ADDRESS");
const { balance, loading, error, refresh } = useBalance(address);

const balanceErrorMessage = computed(() => {
  switch (error.value?.code) {
    case "INVALID_ADDRESS":
      return "Enter a valid Solana address.";
    case "RPC_FAILURE":
      return "Unable to load the balance from RPC.";
    default:
      return null;
  }
});
</script>

<template>
  <section>
    <p>Lamports: {{ balance }}</p>
    <p v-if="loading">Loading...</p>
    <p v-if="balanceErrorMessage">{{ balanceErrorMessage }}</p>
    <button type="button" @click="refresh">Refresh</button>
  </section>
</template>

오류 처리

Composable error ref는 @vue-solana/core/errorsSolanaError | null을 사용합니다. 사용자-facing UI에는 error.value.code로 분기하고, 원래 wallet, RPC, address parsing, timeout, storage failure 디버깅에는 error.value.cause를 보관하세요.

const message = computed(() => {
  switch (error.value?.code) {
    case "NO_WALLET_SELECTED":
      return "Choose a wallet first.";
    case "USER_REJECTED":
      return "The wallet request was rejected.";
    case "TRANSACTION_TIMEOUT":
      return "The transaction is taking longer than expected.";
    case "RPC_FAILURE":
      return "The Solana RPC request failed.";
    default:
      return null;
  }
});

watchEffect(() => {
  if (error.value?.cause) {
    console.debug("Original Solana error", error.value.cause);
  }
});

Account Info 읽기

<script setup lang="ts">
import { computed, ref } from "vue";
import { useAccountInfo } from "@vue-solana/vue/useAccountInfo";

const address = ref("PASTE_A_SOLANA_ADDRESS");
const { accountInfo, loading, error, refresh, stopWatching } = useAccountInfo(address, {
  commitment: "confirmed",
  watch: true,
});

const accountInfoErrorMessage = computed(() => {
  switch (error.value?.code) {
    case "INVALID_ADDRESS":
      return "Enter a valid Solana address.";
    case "RPC_FAILURE":
      return "Unable to load account data from RPC.";
    default:
      return null;
  }
});
</script>

<template>
  <section>
    <p>Lamports: {{ accountInfo?.lamports ?? "Unknown" }}</p>
    <p v-if="loading">Loading...</p>
    <p v-if="accountInfoErrorMessage">{{ accountInfoErrorMessage }}</p>
    <button type="button" @click="refresh">Refresh</button>
    <button type="button" @click="stopWatching">Stop watching</button>
  </section>
</template>

useAccountInfo()는 address가 null이면 RPC를 호출하지 않고 state를 clear합니다. 잘못된 address string은 stale accountInfo를 clear하고 error를 설정하며 getAccountInfo()를 호출하지 않습니다. watch: true가 활성화되면 websocket listener는 component unmount 시 자동 제거됩니다. stopWatching()을 호출하면 현재 listener가 제거되고 해당 composable instance에서 자동 재시작이 방지됩니다.

Program Accounts 읽기

<script setup lang="ts">
import { computed, ref } from "vue";
import { useProgramAccounts } from "@vue-solana/vue/useProgramAccounts";

const programId = ref("PASTE_A_SOLANA_PROGRAM_ID");
const { accounts, loading, error, refresh } = useProgramAccounts(programId, {
  commitment: "confirmed",
  filters: [{ dataSize: 165 }],
  dataSlice: { offset: 0, length: 32 },
});

const programAccountsErrorMessage = computed(() => {
  switch (error.value?.code) {
    case "INVALID_ADDRESS":
      return "Enter a valid Solana program id.";
    case "RPC_FAILURE":
      return "Unable to load program accounts from RPC.";
    default:
      return null;
  }
});
</script>

<template>
  <section>
    <p>Accounts: {{ accounts.length }}</p>
    <p v-if="loading">Loading...</p>
    <p v-if="programAccountsErrorMessage">{{ programAccountsErrorMessage }}</p>
    <button type="button" @click="refresh">Refresh</button>
  </section>
</template>

useProgramAccounts()는 program id가 null이면 RPC를 호출하지 않고 state를 clear합니다. 잘못된 program id string은 stale accounts를 clear하고 error를 설정하며 getProgramAccounts()를 호출하지 않습니다.

경고: useProgramAccounts()는 비용이 클 수 있습니다. 각 refresh는 큰 program-owned account set을 scan하고, 상당한 RPC credit을 소비하고, provider rate limit에 걸리거나 timeout될 수 있습니다. High-traffic UI path에서 broad scan을 실행하지 마세요. 프로덕션 read에는 좁은 filter, dataSlice, caching, indexing, pagination strategy, 전용 RPC infrastructure를 사용하세요.

지갑 상태

<script setup lang="ts">
import { useWallet } from "@vue-solana/vue/useWallet";
import { useWallets } from "@vue-solana/vue/useWallets";

const { wallets, selectedWallet, refreshWallets, selectWallet } = useWallets();
const { publicKey, connected, connecting, connect, disconnect } = useWallet();
</script>

<template>
  <section>
    <button type="button" @click="refreshWallets">Refresh Wallets</button>

    <button
      v-for="wallet in wallets"
      :key="wallet.name"
      type="button"
      @click="selectWallet(wallet)"
    >
      {{ wallet.name }}
    </button>

    <p>Selected: {{ selectedWallet?.name ?? "None" }}</p>
    <p>Connected: {{ connected }}</p>
    <p>Public key: {{ publicKey?.toBase58() }}</p>
    <p v-if="connecting">Connecting...</p>
    <button type="button" :disabled="!selectedWallet || connected || connecting" @click="connect">
      Connect
    </button>
    <button type="button" :disabled="!connected" @click="disconnect">Disconnect</button>
  </section>
</template>

Browser extension wallet은 Solana Wallet Standard를 통해 발견됩니다. Android Mobile Wallet Adapter wallet은 @solana-mobile/wallet-standard-mobile을 통해 등록되며 지원되는 Android Chrome 클라이언트에서 같은 useWallets() 목록에 노출됩니다. iOS Phantom, Solflare, Backpack entry는 iOS 브라우저에서 wallet-specific universal link로 노출됩니다. refreshWallets()는 발견된 wallet list만 업데이트하고, selectWallet()은 active wallet만 설정합니다. Page refresh 후 extension이 이전에 승인된 account를 노출하더라도 connect()가 성공하기 전까지 connected는 false입니다.

Desktop native app wallet adapter는 아직 구현되지 않았습니다. Desktop native support에는 wallet-specific protocol link 또는 향후 native Wallet Standard registration이 필요합니다.

Plugin context가 없으면 composable은 inert SSR-safe state를 반환합니다. 실제 RPC와 wallet operation에는 여전히 plugin이 제공한 client context가 필요합니다.

메시지 서명

import { useSignMessage } from "@vue-solana/vue/useSignMessage";
import { useWallet } from "@vue-solana/vue/useWallet";

const { connected, canSignMessage } = useWallet();
const { signature, status, error, execute } = useSignMessage();

if (connected.value && canSignMessage.value) {
  await execute(new TextEncoder().encode("Sign in to example.com"));
}

메시지 서명은 지갑 소유권 또는 인증 challenge를 위한 것입니다. 트랜잭션 서명이 아니며 온체인 상태 변경을 승인하지 않습니다. 메시지 서명을 노출하지 않는 지갑은 canSignMessage를 false로 보고하고 execute()는 unsupported-wallet error로 거부됩니다.

트랜잭션 상태

import { useSignAndSendTransaction } from "@vue-solana/vue/useSignAndSendTransaction";

const { signature, confirmation, status, loading, error, execute } = useSignAndSendTransaction();

await execute(transaction, {
  confirm: true,
  confirmation: { commitment: "confirmed" },
  skipPreflight: false,
});

현재 wallet은 연결되어 있어야 하며 signAndSendTransaction 또는 signTransaction을 지원해야 합니다. Android Mobile Wallet Adapter wallet은 가능할 때 signTransaction과 app-side RPC submission을 선호합니다. 이렇게 하면 wallet이 성공적으로 전송했지만 browser page가 wallet adapter의 반환 signature를 받지 못하는 mobile handoff edge case를 피할 수 있습니다.

confirm: true가 없으면 execute()는 제출 후 반환하고 statussent로 설정합니다. Confirmation을 활성화하면 status는 sending, confirming을 거쳐 요청한 commitment에 맞게 processed, confirmed, finalized 중 하나로 이동합니다. Confirmation timeout 또는 failure가 발생해도 제출된 signature는 유지되므로 앱은 explorer 링크를 보여줄 수 있습니다.

useSignAndSendTransaction()은 wallet adapter가 결과를 반환하지 않는 경우에도 loading을 clear합니다. 이 stale case에서는 error가 설정되고 chain status를 알 수 없을 수 있으므로 retry 전에 연결된 wallet 또는 explorer를 확인하세요.

기존 Signature Confirm

앱에 이미 제출된 signature가 있고 signing/sending과 별도로 특정 commitment를 기다리고 싶다면 useTransactionConfirmation()을 사용하세요.

import { useTransactionConfirmation } from "@vue-solana/vue/useTransactionConfirmation";

const { signature, confirmation, status, loading, error, confirm, reset } =
  useTransactionConfirmation({ commitment: "confirmed", timeoutMs: 60_000 });

await confirm("PASTE_SUBMITTED_SIGNATURE", { commitment: "finalized" });

이 composable은 confirmation timeout 또는 RPC failure가 발생해도 제출된 signature를 유지하므로, 앱은 사용자에게 error를 표시하면서도 explorer 링크를 계속 보여줄 수 있습니다.

Signature Status 추적

import { useSignatureStatus } from "@vue-solana/vue/useSignatureStatus";

const { status, loading, error, refresh, stopPolling, stopSubscription } = useSignatureStatus(
  "PASTE_SUBMITTED_SIGNATURE",
  {
    pollIntervalMs: 5_000,
    searchTransactionHistory: true,
    subscribe: true,
    commitment: "confirmed",
  },
);

Polling은 interval마다 getSignatureStatuses()를 사용하므로 UI에 update가 더 이상 필요하지 않으면 polling을 중지하세요. stopPolling()은 현재 interval을 clear하고 해당 composable instance에서 automatic polling restart를 방지합니다. 잘못된 signature는 stale status를 clear하고 error를 설정하며 RPC를 호출하거나 polling을 시작하지 않습니다. 0 이하의 잘못된 pollIntervalMs 값은 RangeError를 설정하고 polling을 시작하지 않습니다. subscribe: trueonSignature()를 사용하고 component unmount 시 listener를 제거합니다. stopSubscription()은 현재 signature listener를 제거하고 해당 composable instance에서 automatic restart를 방지합니다.

예제 앱

완전한 실행 가능한 Vue와 Vite 흐름은 Vue Vite 예제를 참고하세요.