Solana 监控 Helius 注册与使用
refer to:
https://dashboard.helius.dev/signup?redirectTo=onboarding
注意:这个服务挑IP,有的IP可用,有的则不可用。所以发现收不到webhook时,多换几个IP试试。
申请api key

操作webhook进行监听
通过查看文档可知:
每次修改 address,一次10 credit
每次触发一个webhook通知,消耗1credit
每个webhook最多支持10w个地址
花了一个下午的时间,终于调通了。 原因在于香港的部分IP不好用。我的是1/3 的可以用。
所以,记得切换IP,如果没反应的话,也可以使用 webhook.site来调试

然后根据这个webhook接收到的内容,直接产生deposit记录即可。
下面是对token ( spl-token )的解析
# token transfer
# 这个会一直存在。 是手续费
# "nativeTransfers": [
# {
# "amount": 2039280,
# "fromUserAccount": "4ZSmwQHYigp3LhRjpV1D3fjAtHZmK1jdM63oXjPW6e7o",
# "toUserAccount": "5kcAo627SZ54oS77ueZp8pYPyJaMdNkdk8Khb6yTgH5D"
# }
# ],
# "signature": "oogvzJoiywY6pby2uvQJrRhSuNfFBuekdHAhZSeBg8dBmwPTbFDLo7fCkvSWDyy8uvWUKNc1u47nyX9LSZLByMd",
# "slot": 321631942,
# "source": "SOLANA_PROGRAM_LIBRARY",
# "timestamp": 1739946105,
#
# # 存在这个,就是token 的转账。
# "tokenTransfers": [
# {
# "fromTokenAccount": "DG57EQFypmJp3jvppNfNRNWd8k8ovNeMFvZY8P8edbEA",
#
# # 发款人地址
# "fromUserAccount": "4ZSmwQHYigp3LhRjpV1D3fjAtHZmK1jdM63oXjPW6e7o",
#
# # 币种的地址 , MY HI TO YOU
# "mint": "7YrqQHvNXs1HGNApmh2gE9RzkxdKLJViJ6bmP7TC6Jsv",
# "toTokenAccount": "5kcAo627SZ54oS77ueZp8pYPyJaMdNkdk8Khb6yTgH5D",
#
# # 收款人地址
# "toUserAccount": "EapoRNHsvLBa68dXhvzGCMLHbQH5fGq9CZeJj7EQEX6Y",
#
# # 数量
# "tokenAmount": 1.1,
# "tokenStandard": "Fungible"
# }
# ],
下面是SOL的转账:
# # 感觉看这个就可以了。
# "nativeTransfers": [
# {
# "amount": 1000000,
# "fromUserAccount": "FXgMDpzBm3uhUDerB9LomPY1PSPj6CxsGBc8G2bm1qBx",
# "toUserAccount": "4ZSmwQHYigp3LhRjpV1D3fjAtHZmK1jdM63oXjPW6e7o"
# }
# ],
# # 这个就是EVM中的tx
# "signature": "dunwNDgj9ipSArUTyimfUYni56TyqrvZHb3EuQ6Z6SfrMrb4d9NqgcjXpJWp6AGJL4pX9xCDKXrNzk88GWZLVyB",
下面,则是需要对新生成的地址,更新到 helius的服务器上,让它帮我们监听
参考:https://docs.helius.dev/data-streaming/webhooks/api-reference/edit-webhook#code-example
def get_all_addresses chain_short_name
chain = Chain.find_by_short_name chain_short_name
return PaymentAddress.where(chain_id: chain.id).map(&:address).uniq
end
def update_webhook_for_solana
url = "https://api.helius.xyz/v0/webhooks/#{ENV['WEBHOOK_SOLANA_HELIUS_WEBHOOK_ID']}?api-key=#{ENV['WEBHOOK_SOLANA_HELIUS_WEBHOOK_API_KEY']}"
# header应该也要加上用户名和密码。不过貌似curl example中没有,也能正常工作
headers = {
'Content-Type' => 'application/json'
}
# 注意:这里需要把所有参数都加上才行,一个都不能少。
# https://docs.helius.dev/data-streaming/webhooks/api-reference/edit-webhook#code-example
body = {
webhookURL: ENV['WEBHOOK_SOLANA_HELIUS_WEBHOOK_URL'],
webhookType: "enhanced",
authHeader: ENV['WEBHOOK_SOLANA_HELIUS_WEBHOOK_AUTH'],
transactionTypes: [ "ANY" ],
accountAddresses: get_all_addresses('sol'),
txnStatus: "all"
}.to_json
tried = 0
begin
response = HTTParty.put(url, headers: headers, body: body)
data = JSON.parse(response.body)
$logger.info "=== after update webhook, response: #{data.inspect}"
rescue Excpetion => e
$logger.error "== :#{e.inspect}"
$logger.error "== :#{e.backtrace.join("\n")}"
retry if tried <= 10
tried += 1
end
end
使用API来进行转账
直接看源代码吧,查询余额,转账。都在这里了。
require('dotenv').config();
const fastify = require('fastify')({
logger: true
});
//const { Keypair } = require('@solana/web3.js');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
// 定义密钥文件目录
const keysDirectory = '/opt/app/solana_keys';
// 定义特殊参数
const my_secret = process.env.MY_SECRET;
const { Connection, PublicKey, Keypair, Transaction, SystemProgram, LAMPORTS_PER_SOL, sendAndConfirmTransaction } = require('@solana/web3.js');
const { getOrCreateAssociatedTokenAccount, createTransferInstruction, TOKEN_PROGRAM_ID, getMint } = require('@solana/spl-token');
// 连接到 Solana 主网
const connection = new Connection('https://api.mainnet-beta.solana.com', "confirmed");
const sender_private_key = Uint8Array.from(JSON.parse(process.env.COINBASE_PRIVATE_KEY));
// 确保密钥目录存在
if (!fs.existsSync(keysDirectory)) {
fs.mkdirSync(keysDirectory, { recursive: true });
}
// 心跳用
fastify.get('/', async (request, reply) => {
return reply.send({ time: new Date()});
})
// 创建账户
fastify.post('/create_account', async (request, reply) => {
const { my_secret: providedMySecret } = request.body;
if (providedMySecret!== my_secret) {
return reply.code(403).send({ error: 'Invalid secret' });
}
try {
// 生成 Solana 密钥对
const keypair = Keypair.generate();
const privateKey = keypair.secretKey.toString();
const address = keypair.publicKey.toString();
// 生成文件名
const timestamp = new Date().toISOString().replace(/[:.-]/g, '-');
const fileName = `${timestamp}_${address}.key`;
const filePath = path.join(keysDirectory, fileName);
// 保存私钥到文件
fs.writeFileSync(filePath, privateKey);
return reply.send({ address });
} catch (error) {
console.error('Error generating wallet:', error);
return reply.code(500).send({ error: 'Internal server error' });
}
});
// GET /get_private_key
fastify.get('/get_private_key', async (request, reply) => {
const { my_secret: providedMySecret, address } = request.query;
if (providedMySecret!== my_secret) {
return reply.code(403).send({ error: 'Invalid secret' });
}
if (!address) {
return reply.code(400).send({ error: 'Address parameter is required' });
}
try {
// 读取密钥文件目录下的所有文件
const files = fs.readdirSync(keysDirectory);
// 遍历文件
for (const file of files) {
// 从文件名中提取地址
const parts = file.split('_');
const addressInFile = parts[parts.length - 1].replace('.key', '');
// 检查地址是否匹配
if (addressInFile === address) {
// 读取私钥文件内容
const filePath = path.join(keysDirectory, file);
const privateKey = fs.readFileSync(filePath, 'utf8');
return reply.send({ privateKey });
}
}
// 如果没有找到匹配的地址
return reply.code(404).send({ error: 'Private key not found for the given address' });
} catch (error) {
console.error('Error reading keys directory:', error);
return reply.code(500).send({ error: 'Internal server error' });
}
});
// 检查solana的余额
// curl 'http://localhost:6800/check_balance_for_solana?my_secret=moneymoneyHOME888$$$&address=4ZSmwQHYigp3LhRjpV1D3fjAtHZmK1jdM63oXjPW6e7o'
fastify.get('/check_balance_for_solana', async (request, reply) => {
const { my_secret: providedMySecret, address } = request.query;
if (providedMySecret!== my_secret) {
return reply.code(403).send({ error: 'Invalid secret' });
}
if (!address) {
return reply.code(400).send({ error: 'Address parameter is required' });
}
// 尝试创建 PublicKey 实例
let publicKey = new PublicKey(address);
// 查询账户的 SOL 余额(以 lamports 为单位)
const balanceInLamports = await connection.getBalance(publicKey);
// 将 lamports 转换为 SOL(1 SOL = 10^9 lamports)
const balanceInSOL = balanceInLamports / 1e9;
// 返回查询结果
reply.send({
address: publicKey.toBase58(),
balance: balanceInSOL,
unit: 'SOL'
});
})
// 定义一个路由,用于查询 SPL - Token 余额
// curl 'http://localhost:6800/check_balance_for_spl_token?my_secret=moneymoneyHOME888$$$&address=4ZSmwQHYigp3LhRjpV1D3fjAtHZmK1jdM63oXjPW6e7o&contract_address=Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB'
fastify.get('/check_balance_for_spl_token', async (request, reply) => {
const { my_secret: providedMySecret, address, contract_address } = request.query;
if (providedMySecret!== my_secret) {
return reply.code(403).send({ error: 'Invalid secret' });
}
// 检查地址和铸造合约地址是否为字符串
if (typeof address!== 'string' || typeof contract_address !== 'string') {
return reply.code(400).send({ error: '地址和铸造合约地址必须是字符串类型' });
}
const owner = new PublicKey(address);
let response = await connection.getParsedTokenAccountsByOwner(owner, {
programId: TOKEN_PROGRAM_ID,
});
result = response.value.filter( (e) => {
return e.account.data["parsed"]["info"]["mint"] == contract_address
})[0]
reply.send({
comment: 'please use uiAmount, do not use amount',
owner: result.account.data["parsed"]["info"]["owner"],
decimals: result.account.data["parsed"]["info"]["tokenAmount"]["decimals"],
uiAmount: result.account.data["parsed"]["info"]["tokenAmount"]["uiAmount"]
})
})
//
// 发送solana
/*
curl --location 'http://154.19.84.62:6800/send_sol' \
--header 'Content-Type: application/json' \
--data '{"my_secret": "moneymoneyHOME888$$$", "to_address": "2D2688ZSDZzJocAGY6m6mxnR3zz6Z8P8qcoKSiegGxT1", "amount": "0.012"}'
*/
// 定义 /send_sol 的 POST 路由
fastify.post('/send_sol', async (request, reply) => {
const { my_secret: providedMySecret, to_address, amount } = request.body;
if (providedMySecret!== my_secret) {
return reply.code(403).send({ error: 'Invalid secret' });
}
// 验证参数
if (!to_address ||!amount) {
return reply.code(400).send({ error: '请提供接收方地址和要发送的 SOL 数量' });
}
// 假设的发送方私钥,需要替换为实际的私钥
const fromKeypair = Keypair.fromSecretKey(sender_private_key);
// 将 SOL 数量转换为 lamports
const lamports = amount * LAMPORTS_PER_SOL;
// 创建交易
const transaction = new Transaction().add(
SystemProgram.transfer({
fromPubkey: fromKeypair.publicKey,
toPubkey: to_address,
lamports,
})
);
// 发送并确认交易
const signature = await sendAndConfirmTransaction(
connection,
transaction,
[fromKeypair]
);
// 返回交易签名
return reply.send({ success: true, signature });
})
// 发送spl-token
fastify.post('/send_spl_token', async (request, reply) => {
const {
my_secret: providedMySecret,
to_address,
amount,
contract_address // 注意参数名改为 contract_address
} = request.body;
// Secret 验证
if (providedMySecret !== my_secret) {
return reply.code(403).send({ error: 'Invalid secret' });
}
// 参数检查
if (!to_address || !amount || !contract_address) {
return reply.code(400).send({ error: 'Missing required parameters' });
}
// 初始化连接
const fromKeypair = Keypair.fromSecretKey(sender_private_key);
try {
// 转换地址
const mintAddress = new PublicKey(contract_address);
const toAddress = new PublicKey(to_address);
// 获取代币精度
const mintInfo = await getMint(connection, mintAddress);
const amountInSmallestUnit = BigInt(Number(amount) * 10 ** mintInfo.decimals);
// 获取或创建发送方 Token Account
const fromTokenAccount = await getOrCreateAssociatedTokenAccount(
connection,
fromKeypair, // payer
mintAddress,
fromKeypair.publicKey,
true // allowOwnerOffCurve
);
// 获取或创建接收方 Token Account
const toTokenAccount = await getOrCreateAssociatedTokenAccount(
connection,
fromKeypair, // payer
mintAddress,
toAddress,
true
);
// 构建交易
const transferInstruction = createTransferInstruction(
fromTokenAccount.address, // source
toTokenAccount.address, // dest
fromKeypair.publicKey, // owner
amountInSmallestUnit, // amount (需使用 BigInt)
[], // signers
TOKEN_PROGRAM_ID
);
// 发送交易
const tx = new Transaction().add(transferInstruction);
const signature = await sendAndConfirmTransaction(
connection,
tx,
[fromKeypair]
);
return reply.send({ success: true, signature });
} catch (error) {
console.error('Transaction failed:', error);
return reply.code(500).send({
error: 'Transaction failed',
details: error.message
});
}
});
// 定义 /send_spl_token 的 POST 路由
fastify.post('/not_send_spl_token', async (request, reply) => {
// 修复参数名:请求中的 contract_address → 代码中的 token_mint_address
const { my_secret: providedMySecret, to_address, amount, contract_address } = request.body;
// 验证 secret
if (providedMySecret !== my_secret) {
return reply.code(403).send({ error: 'Invalid secret' });
}
// 验证参数
if (!to_address || !amount || !contract_address) {
return reply.code(400).send({ error: '请提供接收方地址、发送的 Token 数量和 Token Mint 地址' });
}
// 假设的发送方私钥,需要替换为实际的私钥
const fromKeypair = Keypair.fromSecretKey(sender_private_key);
// 将地址转换为 PublicKey
const toPublicKey = new PublicKey(to_address);
const tokenMintPublicKey = new PublicKey(contract_address); // 使用正确的参数名
// 获取发送方的 Associated Token Account
const fromTokenAccount = await Token.getAssociatedTokenAddress(
TOKEN_PROGRAM_ID,
tokenMintPublicKey,
fromKeypair.publicKey
);
// 获取接收方的 Associated Token Account
const toTokenAccount = await Token.getAssociatedTokenAddress(
TOKEN_PROGRAM_ID,
tokenMintPublicKey,
toPublicKey
);
// 创建 Token 对象(确保已导入 Token 类)
const token = new Token(connection, tokenMintPublicKey, TOKEN_PROGRAM_ID, fromKeypair);
// 检查接收方的 Associated Token Account 是否存在,如果不存在则创建
const toTokenAccountInfo = await connection.getAccountInfo(toTokenAccount);
if (!toTokenAccountInfo) {
const createAccountTransaction = new Transaction().add(
Token.createAssociatedTokenAccountInstruction(
TOKEN_PROGRAM_ID,
tokenMintPublicKey,
toTokenAccount,
toPublicKey,
fromKeypair.publicKey
)
);
await sendAndConfirmTransaction(connection, createAccountTransaction, [fromKeypair])
.catch(error => {
console.error('Error creating associated token account:', error);
return reply.code(500).send({ error: 'Failed to create associated token account', details: error.message });
});
}
// 发送 SPL Token(修复精度转换)
const decimals = await token.getDecimals(); // 动态获取代币精度
const amountInLamports = Number(amount) * Math.pow(10, decimals);
const transaction = new Transaction().add(
Token.createTransferInstruction(
TOKEN_PROGRAM_ID,
fromTokenAccount,
toTokenAccount,
fromKeypair.publicKey,
[],
amountInLamports // 使用动态计算的精度
)
);
// 发送并确认交易
const signature = await sendAndConfirmTransaction(connection, transaction, [fromKeypair])
.catch(error => {
console.error('Error sending SPL Token:', error);
return reply.code(500).send({ error: 'Failed to send SPL Token', details: error.message });
});
// 返回交易签名
return reply.send({ success: true, signature });
});
// 启动服务器
fastify.listen({ port: 6800, host: '0.0.0.0'}, (err) => {
if (err) {
fastify.log.error(err);
process.exit(1);
}
console.log('Server is running on port 6800');
});