Gate 广场「创作者认证激励计划」开启:入驻广场,瓜分每月 $10,000 创作奖励!
无论你是广场内容达人,还是来自其他平台的优质创作者,只要积极创作,就有机会赢取豪华代币奖池、Gate 精美周边、流量曝光等超 $10,000+ 丰厚奖励!
参与资格:
满足以下任一条件即可报名👇
1️⃣ 其他平台已认证创作者
2️⃣ 单一平台粉丝 ≥ 1000(不可多平台叠加)
3️⃣ Gate 广场内符合粉丝与互动条件的认证创作者
立即填写表单报名 👉 https://www.gate.com/questionnaire/7159
✍️ 丰厚创作奖励等你拿:
🎁 奖励一:新入驻创作者专属 $5,000 奖池
成功入驻即可获认证徽章。
首月发首帖(≥ 50 字或图文帖)即可得 $50 仓位体验券(限前100名)。
🎁 奖励二:专属创作者月度奖池 $1,500 USDT
每月发 ≥ 30 篇原创优质内容,根据发帖量、活跃天数、互动量、内容质量综合评分瓜分奖励。
🎁 奖励三:连续活跃创作福利
连续 3 个月活跃(每月 ≥ 30 篇内容)可获 Gate 精美周边礼包!
🎁 奖励四:专属推广名额
认证创作者每月可优先获得 1 次官方项目合作推广机会。
🎁 奖励五:Gate 广场四千万级流量曝光
【推荐关注】资源位、“优质认证创作者榜”展示、每周精选内容推荐及额外精选帖激励,多重曝光助你轻
交易所钱包系统开发——接入 Solana 链
上一篇我们把交易所风控体系补齐,这一篇给交易所钱包接入 Solana 链。Solana 的账户模型、日志存储和确认机制与以太坊系链有很大的不同,如果沿用以太坊套路,还是很容易踩坑的。下面我们梳理一下记录Solana 的整体思路。
了解独特的 Solana
Solana 账户模型
Solana 使用程序与数据分离的模型,程序是可以共用的,而程序的数据是通过 PDA(Program Derived Address)账户单独保存的,由于程序是共用的,因此需要 Token Mint 来区别不同的 Token。Token Mint 账户存储代币的全局元数据,存储例如 铸造权限(mint_authority)、 总供应量(supply)、 小数位数(decimals) 等,
每个代币都有唯一的 Mint 账户地址作为标识符,例如 USD Coin(USDC)在 Solana 主网的 Mint 地址是 EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v。
Solana 上两套 Token 程序,一个是SPL Token,一个是SPL Token-2022,每种 SPL Token 都有独立的 ATA(Associated Token Account)来保存用户的余额,在 Token 转账时,实际上是调用各自的程序在 Token 在 ATA 账户之间在转移。
Solana 日志限制
在以太坊上,是通过解析历史的转账日志来获取 Token 转账的,但是 Solana 的执行日志默认不会永久保留,Solana 的日志不属于账本状态(state)的(也没有日志的布隆过滤器),并且可能在执行过程中截断输出。
因此,我们不能通过“扫描日志”来做充值对账,而是要使用 getBlock 或者 getSignaturesForAddress 来解析指令。
Solana 确认与重组
Solana 出块时间为 400ms,经过 32 个确认(大概 12 s)会达到 finalized , 如果实时性要求不高的话,简单的方法是只信任 finalized 的区块。
如果想要更高的实时性,就需要考虑可能会出现的区块重组,尽管较少出现。但是 Solana 共识不依赖 parentBlockHash 形成链结构,不能像类似以太坊那样通过 parentBlockHash 和数据库中的 blockHash 不一样来判断分叉。那应该使用怎样方法来判断区块被重组了呢?
在本地扫块时,我们要记录 slot 的 blockhash ,如果出现同 slot 的 blockhash 有变化,那就说明发生了回滚。
理解 Solana 的不同,接下来就可以着手实现了,先看看数据库要做怎样的修改:
数据库表设计
由于 Solana 有两种类型的 Token , 因此,我们需要在 tokens 表上,添加一个 token_type 用来区分 spl-token 和 spl-token-2022
Solana 地址尽管和以太坊不一样,但是同样可以通过 BIP32、BIP44 衍生,只不过衍生的路径不一样而已,因此只需要使用原有 wallets 表,但为了支持 ATA 地址映射,Solana 扫块追踪,需要添加以下三张表:
其中:
详细表定义可参考 db_gateway/database.md
处理用户充值
处理用户充值,需要不断地扫描 Solana 链上数据,通常有两个方法:
方法 1 :扫地址的签名,通过调用 getSignaturesForAddress(address, { before, until, limit }),传入我们关注的地址作为参数,这个地址是我们为用户生成的 ATA 地址,也可以是 programID(注意 spl-token 的 transfer 指令调用是不包含 mint 地址的) 并通过控制 before、 until 参数不断的 拉取增量签名,然后再通过getTransaction(signature) 获取交易的信息数据。
这个方法可以适合数据量或账号较少的情况,如果账户数非常大,使用扫块更适合,我们这里就是使用扫块方法。
方法 2:扫块的方法是不断是拿到最新的 Slot, 调用 getBlock(slot) 获取完整的交易详细信息、签名或帐户,然后获取根据指令与账户,过滤出我们所需要的数据。
如果不想自己扫块,还有一个方法是使用第三方 RPC 服务商提供额外的 Indexer 服务, 例如提供 Webhook、账号 Account 监听与高阶的过滤支持,可承担大数据量解析压力。
扫块流程
我们使用了方法二,相关代码在 scan/solana-scan 模块下的 blockScanner.ts 和 txParser.ts,主要流程如下:
1. 初始同步阶段、补历史区块(performInitialSync)
2. 扫描阶段(scanNewSlots)
3. 区块解析(txParser.parseBlock)
4. 指令解析(txParser.parseInstruction)
回滚具体处理:
程序会不断的获取 finalizedSlot,当 slot ≤ finalizedSlot 时标记为 finalized,对于依旧在 confirmed 状态的块,判断 blockhash 是否更改来判断回滚。
示例核心代码如下:
// blockScanner.ts - 扫描单个槽位
async scanSingleSlot(slot: number) {
const block = await solanaClient.getBlock(slot);
if (!block) {
await insertSlot({ slot, status: ‘skipped’ });
return;
}
const finalizedSlot = await getCachedFinalizedSlot();
const status = slot <= finalizedSlot ? ‘finalized’ : ‘confirmed’;
await processBlock(slot, block, status);
}
// txParser.ts - 解析转账指令
for (const tx of block.transactions) {
if (tx.meta?.err) continue; // 跳过失败的交易
const instructions = [
…tx.transaction.message.instructions,
…(tx.meta.innerInstructions ?? []).flatMap(i => i.instructions)
];
for (const ix of instructions) {
// SOL 转账
if (ix.programId === SYSTEM_PROGRAM_ID && ix.parsed?.type === ‘transfer’) {
if (monitoredAddresses.has(ix.parsed.info.destination)) {
// …
}
}
// Token 转账
if (ix.programId === TOKEN_PROGRAM_ID || ix.programId === TOKEN_2022_PROGRAM_ID) {
if (ix.parsed?.type === ‘transfer’ || ix.parsed?.type === ‘transferChecked’)) {
const ataAddress = ix.parsed.info.destination; // ATA 地址
const walletAddress = ataToWalletMap.get(ataAddress); // 映射到钱包地址
if (walletAddress && monitoredAddresses.has(walletAddress)) {
// …
}
}
}
}
}
在扫描到充值交易后,沿用 DB Gateway + 风控双签名的安全,在验证之后,将数据写入的资金流水表中 credits。
提现
Solana 的提现流程与 EVM 链类似,但在交易构建有差异:
提现流程
其实这里把获取交易 Blockhash 放在风控检查之后更好
Signer 模块签名交易核心代码如下:
根据交易类型构建不同的指令:
// SOL 转账指令
const instruction = getTransferSolInstruction({
source: hotWalletSigner,
destination: solanaAddress(to),
amount: BigInt(amount)
});
// 2. 构建 Token 转账指令
const instruction = getTransferInstruction({
source: sourceAta,
destination: destAta,
authority: hotWalletSigner,
amount: BigInt(amount)
});
构建并签名交易消息:
// 使用 @solana/kit 构建交易
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
tx => setTransactionMessageFeePayerSigner(hotWalletSigner, tx),
tx => setTransactionMessageLifetimeUsingBlockhash({
blockhash: blockhash,
lastValidBlockHeight: BigInt(lastValidBlockHeight)
}, tx),
tx => appendTransactionMessageInstruction(instruction, tx)
);
// 签名交易
const signedTx = await signTransactionMessageWithSigners(transactionMessage);
// 返回两种编码:
// 1. Base64 编码的完整交易(用于发送到网络)
const signedTransaction = getBase64EncodedWireTransaction(signedTx);
Wallet 模块发送交易到网络
// 使用 @solana/web3.js 发送交易
const solanaRpc = chainConfigManager.getSolanaRpc();
const txSignature = await solanaRpc.sendTransaction(
signedTransaction, // Base64 编码的交易
…
);
完整的提现实现代码位于:
注意这里有两个待实现的优化:
总结
交易所接入 Solana 链在总体架构上没有变化,关键是适配其独特的账户模型、交易结构以及共识确认机制。
在处理充值时预先建立并维护 ATA 到钱包地址的映射表,用于 Token 转账识别,统一监控 blockhash 变化检测区块重组,动态更新交易状态(confirmed → finalized)。
在提现时,使用 getLatestBlockhash() 获取交易参数,同时区分 Sol、 SPL Token 和 Token-2022 来构造不同的交易。