透過 BlockChain RPC 調用 Solana 合約

獲取 RPC 地址

首先,需要訪問 https://zan.top/service/apikeys 獲取 RPC 地址:

需要登錄 ZAN,ZAN 會自動為你創建一個免費的 Access Key,對於其他大部分 BlockChain RPC 服務提供商,也是類似的邏輯,你都需要先獲得一個地址,這個地址就是調用 RPC 服務的 Endpoint。

通過命令行訪問

你可以通過以下代碼直接在 Shell 命令行中訪問 PRC 測試是否可以聯通:

curl --request POST \
     --url https://api.zan.top/node/v1/solana/mainnet/${YOUR_ACCESS_KEY} \
     --header 'accept: application/json' \
     --header 'content-type: application/json' \
     --data '
{
  "id": 1,
  "jsonrpc": "2.0",
  "method": "getBlockHeight"
}
'

需要注意的是,${YOUR_ACCESS_KEY} 需要替換為你自己的 Key。你也可以直接通過 ZAN 網站上提供的代碼來運行:

然後你可以看到輸出類似如下的結果:

{"result":333804817,"id":1,"jsonrpc":"2.0"}

這就表明 RPC 調用成功了。

通過 JS SDK 調用合約

Solana 官方提供了一個 SDK @solana/rpc,它是 @solana/kit 包的一部分。

你可以通過以下方式調用 Solana 鏈 RPC:

import { createSolanaRpc } from '@solana/kit';

// Create an RPC client.
const rpc = createSolanaRpc('https://api.zan.top/node/v1/solana/mainnet/${YOUR_ACCESS_KEY}');

// Send a request.
const slot = await rpc.getSlot().send();


如果是需要在瀏覽器端喚起錢包進行簽名,那麼還需要借助 @wallet-standard/react 來獲取錢包,以及喚起錢包獲得簽名。

以下是一個完整的 React 組件示例:

import { Blockquote, Box, Button, Dialog, Flex, Link, Select, Text, TextField } from '@radix-ui/themes';
import {
    address,
    appendTransactionMessageInstruction,
    assertIsTransactionMessageWithSingleSendingSigner,
    createTransactionMessage,
    getBase58Decoder,
    lamports,
    pipe,
    setTransactionMessageFeePayerSigner,
    setTransactionMessageLifetimeUsingBlockhash,
    signAndSendTransactionMessageWithSigners,
} from '@solana/kit';
import { useWalletAccountTransactionSendingSigner } from '@solana/react';
import { getTransferSolInstruction } from '@solana-program/system';
import { getUiWalletAccountStorageKey, type UiWalletAccount, useWallets } from '@wallet-standard/react';
import type { SyntheticEvent } from 'react';
import { useContext, useId, useMemo, useRef, useState } from 'react';
import { useSWRConfig } from 'swr';

import { ChainContext } from '../context/ChainContext';
import { RpcContext } from '../context/RpcContext';
import { ErrorDialog } from './ErrorDialog';
import { WalletMenuItemContent } from './WalletMenuItemContent';

type Props = Readonly<{
    account: UiWalletAccount;
}>;

function solStringToLamports(solQuantityString: string) {
    if (Number.isNaN(parseFloat(solQuantityString))) {
        throw new Error('Could not parse token quantity: ' + String(solQuantityString));
    }
    const numDecimals = BigInt(solQuantityString.split('.')[1]?.length ?? 0);
    const bigIntLamports = BigInt(solQuantityString.replace('.', '')) * 10n ** (9n - numDecimals);
    return lamports(bigIntLamports);
}

export function SolanaSignAndSendTransactionFeaturePanel({ account }: Props) {
    const { mutate } = useSWRConfig();
    const { current: NO_ERROR } = useRef(Symbol());
    const { rpc } = useContext(RpcContext);
    const wallets = useWallets();
    const [isSendingTransaction, setIsSendingTransaction] = useState(false);
    const [error, setError] = useState(NO_ERROR);
    const [lastSignature, setLastSignature] = useState<Uint8Array | undefined>();
    const [solQuantityString, setSolQuantityString] = useState<string>('');
    const [recipientAccountStorageKey, setRecipientAccountStorageKey] = useState<string | undefined>();
    const recipientAccount = useMemo(() => {
        if (recipientAccountStorageKey) {
            for (const wallet of wallets) {
                for (const account of wallet.accounts) {
                    if (getUiWalletAccountStorageKey(account) === recipientAccountStorageKey) {
                        return account;
                    }
                }
            }
        }
    }, [recipientAccountStorageKey, wallets]);
    const { chain: currentChain, solanaExplorerClusterName } = useContext(ChainContext);
    const transactionSendingSigner = useWalletAccountTransactionSendingSigner(account, currentChain);
    const lamportsInputId = useId();
    const recipientSelectId = useId();
    return (
        <Flex asChild gap="2" direction={{ initial: 'column', sm: 'row' }} style={{ width: '100%' }}>
            <form
                onSubmit={async e => {
                    e.preventDefault();
                    setError(NO_ERROR);
                    setIsSendingTransaction(true);
                    try {
                        const amount = solStringToLamports(solQuantityString);
                        if (!recipientAccount) {
                            throw new Error('The address of the recipient could not be found');
                        }
                        const { value: latestBlockhash } = await rpc
                            .getLatestBlockhash({ commitment: 'confirmed' })
                            .send();
                        const message = pipe(
                            createTransactionMessage({ version: 0 }),
                            m => setTransactionMessageFeePayerSigner(transactionSendingSigner, m),
                            m => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m),
                            m =>
                                appendTransactionMessageInstruction(
                                    getTransferSolInstruction({
                                        amount,
                                        destination: address(recipientAccount.address),
                                        source: transactionSendingSigner,
                                    }),
                                    m,
                                ),
                        );
                        assertIsTransactionMessageWithSingleSendingSigner(message);
                        const signature = await signAndSendTransactionMessageWithSigners(message);
                        void mutate({ address: transactionSendingSigner.address, chain: currentChain });
                        void mutate({ address: recipientAccount.address, chain: currentChain });
                        setLastSignature(signature);
                        setSolQuantityString('');
                    } catch (e) {
                        setLastSignature(undefined);
                        setError(e);
                    } finally {
                        setIsSendingTransaction(false);
                    }
                }}
            >
                <Box flexGrow="1" overflow="hidden">
                    <Flex gap="3" align="center">
                        <Box flexGrow="1" minWidth="90px" maxWidth="130px">
                            <TextField.Root
                                disabled={isSendingTransaction}
                                id={lamportsInputId}
                                placeholder="Amount"
                                onChange={(e: SyntheticEvent<HTMLInputElement>) =>
                                    setSolQuantityString(e.currentTarget.value)
                                }
                                style={{ width: 'auto' }}
                                type="number"
                                value={solQuantityString}
                            >
                                <TextField.Slot side="right">{'\u25ce'}</TextField.Slot>
                            </TextField.Root>
                        </Box>
                        <Box flexShrink="0">
                            <Text as="label" color="gray" htmlFor={recipientSelectId} weight="medium">
                                To Account
                            </Text>
                        </Box>
                        <Select.Root
                            disabled={isSendingTransaction}
                            onValueChange={setRecipientAccountStorageKey}
                            value={recipientAccount ? getUiWalletAccountStorageKey(recipientAccount) : undefined}
                        >
                            <Select.Trigger
                                style={{ flexGrow: 1, flexShrink: 1, overflow: 'hidden' }}
                                placeholder="Select a Connected Account"
                            />
                            <Select.Content>
                                {wallets.flatMap(wallet =>
                                    wallet.accounts
                                        .filter(({ chains }) => chains.includes(currentChain))
                                        .map(account => {
                                            const key = getUiWalletAccountStorageKey(account);
                                            return (
                                                <Select.Item key={key} value={key}>
                                                    <WalletMenuItemContent wallet={wallet}>
                                                        {account.address}
                                                    </WalletMenuItemContent>
                                                </Select.Item>
                                            );
                                        }),
                                )}
                            </Select.Content>
                        </Select.Root>
                    </Flex>
                </Box>
                <Dialog.Root
                    open={!!lastSignature}
                    onOpenChange={open => {
                        if (!open) {
                            setLastSignature(undefined);
                        }
                    }}
                >
                    <Dialog.Trigger>
                        <Button
                            color={error ? undefined : 'red'}
                            disabled={solQuantityString === '' || !recipientAccount}
                            loading={isSendingTransaction}
                            type="submit"
                        >
                            Transfer
                        </Button>
                    </Dialog.Trigger>
                    {lastSignature ? (
                        <Dialog.Content
                            onClick={e => {
                                e.stopPropagation();
                            }}
                        >
                            <Dialog.Title>You transferred tokens!</Dialog.Title>
                            <Flex direction="column" gap="2">
                                <Text>Signature:</Text>
                                <Blockquote>{getBase58Decoder().decode(lastSignature)}</Blockquote>
                                <Text>
                                    <Link
                                        href={`https://explorer.solana.com/tx/${getBase58Decoder().decode(
                                            lastSignature,
                                        )}?cluster=${solanaExplorerClusterName}`}
                                        target="_blank"
                                    >
                                        View this transaction
                                    </Link>{' '}
                                    on Explorer
                                </Text>
                            </Flex>
                            <Flex gap="3" mt="4" justify="end">
                                <Dialog.Close>
                                    <Button>Cool!</Button>
                                </Dialog.Close>
                            </Flex>
                        </Dialog.Content>
                    ) : null}
                </Dialog.Root>
                {error !== NO_ERROR ? (
                    <ErrorDialog error={error} onClose={() => setError(NO_ERROR)} title="Transfer failed" />
                ) : null}
            </form>
        </Flex>
    );
}


更多的文檔和示例可以參閱 Solana 官方的 Github 倉庫:https://github.com/anza-xyz/kit