透過 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