6 分钟阅读

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来调试

https://webhook.site/#!/view/77ec25a5-b35b-4999-8f11-28a08938c59e/37db8d57-cd24-4c6a-b219-d9bc4e8748f5/1

然后根据这个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');
});



更新时间: