大数跨境
0
0

在 Solana 上实现捆绑交易(atomic / bundled swap)

在 Solana 上实现捆绑交易(atomic / bundled swap) 链块高度
2025-12-05
4
导读:清晰的说明 + 完整、可运行的代码示例,把 “在 Solana 上实现捆绑交易(atomic / bundled swap)” 的两种主流实现方式讲清楚。

清晰的说明 + 完整、可运行的代码示例,把 “在 Solana 上实现捆绑交易(atomic / bundled swap)” 的两种主流实现方式讲清楚:

方法 A(最简单、推荐)—— 直接在同一笔交易中放入多条 transfer 指令:由交易的多个参与者在同一笔交易上签名,交易中包含双方的 token transfer 指令,若有一条失败则整笔交易回滚。

方法 B(更结构化、可扩展)—— 使用一个 on-chain 程序(Anchor)来执行原子交换:程序要求双方在同一笔交易中作为 signer,程序通过 CPI 调用 token program 完成两笔转账,失败则整笔回滚。该方式便于加入额外检查(最小价格、nonce、防重放等)。

下面我将分别说明概念、先决条件、完整代码(客户端 TypeScript / Node.js 示例 + Anchor 程序示例),以及如何运行与测试。代码尽量详尽,足够拷贝、运行与理解。

背景知识(简短)

  • 在 Solana 中,一笔交易可以包含多条 instruction(跨程序调用也行),并且交易所包含的所有 instruction 要么全部执行成功要么全部失败 —— 这保证了“同一笔交易内的原子性”。

  • spl-token 的 Transfer 指令需要来源 token account 的 owner 签名(或预先 Approve 给 delegate)。因此,若要在同一笔 tx 中把 A 的代币给 B 并且把 B 的代币给 A,需要 A 和 B 都在该交易上签名(或先做 approve)。

  • 如果希望由一个程序来担保交易(例如做更多验证),程序可以在交易中执行 CPI(跨合约调用),并且可以利用顶层交易的签名位来做 token CPI(即程序可以在 CPI 中把顶层的签名者标记为 is_signer,token program 会认可)。

前提与准备

  • Node.js >= 18(示例用 TypeScript + @solana/web3.js + @solana/spl-token)

  • Anchor(如果使用方法 B):Rust + Anchor CLI(anchor)已安装。示例基于 Anchor v0.28+(请用你本地的 Anchor 版本相匹配)。

  • 使用 devnet 或 localnet(示例我用 devnet);若使用 localnet,请确保 solana-test-validator 在运行。

  • 安装依赖:@solana/web3.js 与 @solana/spl-token(见下面每个示例的安装步骤)。

方法 A:在同一个交易中直接放两条 transfer 指令(客户端实现)

适用场景:双方即时在线、都能在客户端签名 —— 最简洁高效的原子交换方式。

核心思想(一句话)

把 Transfer A→B 和 Transfer B→A 两条 instruction 放在同一笔 transaction 中,要求 A 和 B 都对该交易签名。若任一条失败(账户余额不足、signature 缺失等),整个交易回滚。

完整示例(TypeScript)

下面的示例包含以下步骤(在 devnet 上跑):

  1. 创建两个 test 钱包(payer、userA、userB);

  2. 创建两个 SPL Token Mint(tokenA、tokenB)并 mint 给各自账户;

  3. 构建一笔交易,包含两条 transfer instruction:A 的 tokenA 转给 B;B 的 tokenB 转给 A;

  4. 让 A 与 B 共同签名并发送交易;

  5. 验证转账结果。

注意:示例为了演示方便在脚本中直接生成 Keypair 并 airdrop 测试 SOL。生产环境请用钱包提供者(Phantom、Globe 等)签名或使用更加安全的 key 管理。

安装依赖

mkdir solana-atomic-swap && cd solana-atomic-swap
npm init -y
npm i @solana/web3.js @solana/spl-token @types/node typescript ts-node
npx tsc --init

atomic_swap_direct.ts(完整脚本)

将下面代码保存为 atomic_swap_direct.ts,然后用 npx ts-node atomic_swap_direct.ts 运行(示例基于 devnet)。

/**
 * atomic_swap_direct.ts
 * 演示:在同一笔交易中把 tokenA 从 Alice 转到 Bob,同时把 tokenB 从 Bob 转到 Alice
 * 要求 Alice 和 Bob 都对该交易签名 —— 原子执行
 */

import {
  Keypair,
  Connection,
  clusterApiUrl,
  LAMPORTS_PER_SOL,
  PublicKey,
  Transaction,
  sendAndConfirmTransaction,
} from "@solana/web3.js";
import {
  createMint,
  getOrCreateAssociatedTokenAccount,
  mintTo,
  transfer,
  getMint,
  TOKEN_PROGRAM_ID,
} from "@solana/spl-token";

async functionmain() {
  const connection = new Connection(clusterApiUrl("devnet"), "confirmed");

  // 1) 准备三个钱包:fee payer / Alice / Bob
  const feePayer = Keypair.generate(); // 脚本费用支付者
  const alice = Keypair.generate();
  const bob = Keypair.generate();

  // airdrop SOL
for (const kp of [feePayer, alice, bob]) {
    const sig = await connection.requestAirdrop(kp.publicKey, LAMPORTS_PER_SOL * 2);
    await connection.confirmTransaction(sig, "confirmed");
  }
  console.log("Airdrop done.");

  // 2) 创建两个 Mint:tokenA(decimals=6)和 tokenB(decimals=6)
  const decimals = 6;
  const tokenAMint = await createMint(connection, feePayer, feePayer.publicKey, null, decimals);
  const tokenBMint = await createMint(connection, feePayer, feePayer.publicKey, null, decimals);
  console.log("Created mints:", tokenAMint.toBase58(), tokenBMint.toBase58());

  // 3) Alice 创建她的 ATA 并 mint 一些 tokenA 给她
  const aliceTokenAAccount = await getOrCreateAssociatedTokenAccount(
    connection,
    feePayer,
    tokenAMint,
    alice.publicKey
  );

  // Bob 创建他自己的 ATA for tokenB,并 mint 给他
  const bobTokenBAccount = await getOrCreateAssociatedTokenAccount(
    connection,
    feePayer,
    tokenBMint,
    bob.publicKey
  );

  // mint initial balances: Alice has 100 tokenA, Bob has 200 tokenB
  const aliceAmount = BigInt(100) * BigInt(10 ** decimals); // use BigInt to avoid fp
  const bobAmount = BigInt(200) * BigInt(10 ** decimals);

  await mintTo(connection, feePayer, tokenAMint, aliceTokenAAccount.address, feePayer, Number(aliceAmount));
  await mintTo(connection, feePayer, tokenBMint, bobTokenBAccount.address, feePayer, Number(bobAmount));
  console.log("Minted initial balances.");

  // create the counterpart ATAs (the target accounts for the swap)
  const bobTokenAAccount = await getOrCreateAssociatedTokenAccount(
    connection,
    feePayer,
    tokenAMint,
    bob.publicKey
  );
  const aliceTokenBAccount = await getOrCreateAssociatedTokenAccount(
    connection,
    feePayer,
    tokenBMint,
    alice.publicKey
  );

  // show before balances
  const mintAInfo = await getMint(connection, tokenAMint);
  console.log(`Decimals for tokenA: ${mintAInfo.decimals}`);
  console.log("Before balances:");
  console.log("Alice tokenA:", (await connection.getTokenAccountBalance(aliceTokenAAccount.address)).value.uiAmountString);
  console.log("Alice tokenB:", (await connection.getTokenAccountBalance(aliceTokenBAccount.address)).value.uiAmountString);
  console.log("Bob tokenA:", (await connection.getTokenAccountBalance(bobTokenAAccount.address)).value.uiAmountString);
  console.log("Bob tokenB:", (await connection.getTokenAccountBalance(bobTokenBAccount.address)).value.uiAmountString);

  // 4) 构建原子交易:Alice 给 Bob 10 tokenA,Bob 给 Alice 20 tokenB
  const sendAmountA = BigInt(10) * BigInt(10 ** decimals);
  const sendAmountB = BigInt(20) * BigInt(10 ** decimals);

  // 使用 spl-token 的 transfer 工具生成两条 transfer 指令
  const ixAtoB = transfer(
    // transfer 的 helper 返回 TransactionInstruction
    // 参数:connection, payer, source, destination, owner, amount
    // 这里我们只用其 signature-friendly form:create instruction manually via helper is nontrivial.
    // 因此使用 transfer from spl-token v0.3 helper that returns tx signature when executed.
    // 为了把 instruction 放入 Transaction,我们需要使用 createTransferInstruction(low-level)
    // But for simplicity, use low-level createTransferInstruction from spl-token package
    // We'll import it below.
    {} as any
  );

  // NOTE: Above comment: the helper `transfer` executes a transaction.
  // For building instruction-only, use createTransferInstruction.
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});

上面脚本演示思路清楚,但为了保持代码易读与可复制,我将给出完整且可运行的 atomic_swap_direct.ts(包含实际构建 instruction 的方式)。下面是可直接运行的最终版本(已用 createTransferInstruction):

请用以下完整最终脚本替换上面占位内容并运行:

// atomic_swap_direct.ts (final runnable)
// 注意:需要 @solana/spl-token v0.3.x 或更高,且 Node 支持 BigInt
import {
  Keypair,
  Connection,
  clusterApiUrl,
  LAMPORTS_PER_SOL,
  PublicKey,
  Transaction,
  sendAndConfirmTransaction,
  Signer,
} from "@solana/web3.js";
import {
  createMint,
  getOrCreateAssociatedTokenAccount,
  mintTo,
  getMint,
  createTransferInstruction,
  TOKEN_PROGRAM_ID,
} from "@solana/spl-token";

async function airdropIfNeeded(connection: Connection, pubkey: PublicKey) {
  const lamports = (await connection.getBalance(pubkey)) || 0;
if (lamports < LAMPORTS_PER_SOL) {
    const sig = await connection.requestAirdrop(pubkey, LAMPORTS_PER_SOL * 2);
    await connection.confirmTransaction(sig, "confirmed");
  }
}

async functionmain() {
  const connection = new Connection(clusterApiUrl("devnet"), "confirmed");

  const feePayer = Keypair.generate();
  const alice = Keypair.generate();
  const bob = Keypair.generate();

  // Airdrop
  await Promise.all([airdropIfNeeded(connection, feePayer.publicKey), airdropIfNeeded(connection, alice.publicKey), airdropIfNeeded(connection, bob.publicKey)]);
  console.log("Airdrop done.");

  // Create Mints
  const decimals = 6;
  const tokenAMint = await createMint(connection, feePayer, feePayer.publicKey, null, decimals);
  const tokenBMint = await createMint(connection, feePayer, feePayer.publicKey, null, decimals);
  console.log("Created token mints:", tokenAMint.toBase58(), tokenBMint.toBase58());

  // Create ATAs
  const aliceTokenAAccount = await getOrCreateAssociatedTokenAccount(connection, feePayer, tokenAMint, alice.publicKey);
  const aliceTokenBAccount = await getOrCreateAssociatedTokenAccount(connection, feePayer, tokenBMint, alice.publicKey);
  const bobTokenAAccount = await getOrCreateAssociatedTokenAccount(connection, feePayer, tokenAMint, bob.publicKey);
  const bobTokenBAccount = await getOrCreateAssociatedTokenAccount(connection, feePayer, tokenBMint, bob.publicKey);

  // Mint initial tokens: Alice 100 tokenA, Bob 200 tokenB
  const aliceAmount = 100n * 10n ** BigInt(decimals);
  const bobAmount = 200n * 10n ** BigInt(decimals);
  await mintTo(connection, feePayer, tokenAMint, aliceTokenAAccount.address, feePayer, Number(aliceAmount));
  await mintTo(connection, feePayer, tokenBMint, bobTokenBAccount.address, feePayer, Number(bobAmount));
  console.log("Minted initial supplies.");

  // Show balances before
  console.log("Before swap:");
  console.log("Alice tokenA:", (await connection.getTokenAccountBalance(aliceTokenAAccount.address)).value.uiAmountString);
  console.log("Alice tokenB:", (await connection.getTokenAccountBalance(aliceTokenBAccount.address)).value.uiAmountString);
  console.log("Bob tokenA:", (await connection.getTokenAccountBalance(bobTokenAAccount.address)).value.uiAmountString);
  console.log("Bob tokenB:", (await connection.getTokenAccountBalance(bobTokenBAccount.address)).value.uiAmountString);

  // Build atomic transaction: Alice -> Bob 10 tokenA; Bob -> Alice 20 tokenB
  const sendAmountA = 10n * 10n ** BigInt(decimals); // 10 tokenA
  const sendAmountB = 20n * 10n ** BigInt(decimals); // 20 tokenB

  // Create transfer instructions (token program)
  const ixAtoB = createTransferInstruction(aliceTokenAAccount.address, bobTokenAAccount.address, alice.publicKey, Number(sendAmountA), [], TOKEN_PROGRAM_ID);
  const ixBtoA = createTransferInstruction(bobTokenBAccount.address, aliceTokenBAccount.address, bob.publicKey, Number(sendAmountB), [], TOKEN_PROGRAM_ID);

  // Construct transaction and add two instructions
  const tx = new Transaction();
  tx.add(ixAtoB);
  tx.add(ixBtoA);

  // Set fee payer and recent blockhash will be populated by RPC send method
  tx.feePayer = feePayer.publicKey;

  // Partial sign by feePayer (payer covers tx fee)
  tx.partialSign(feePayer);

  // At this point, the tx needs Alice and Bob signatures as owners of source accounts.
  // We will have Alice and Bob sign the tx.
  // Serialize transaction message for them to sign:
  tx.recentBlockhash = (await connection.getLatestBlockhash("confirmed")).blockhash;

  // Sign with Alice and Bob
  tx.partialSign(alice);
  tx.partialSign(bob);

  // Serialize and send
  const raw = tx.serialize();
  const txid = await connection.sendRawTransaction(raw);
  console.log("Sent atomic swap txid:", txid);
  await connection.confirmTransaction(txid, "confirmed");
  console.log("Transaction confirmed.");

  // Balances after
  console.log("After swap:");
  console.log("Alice tokenA:", (await connection.getTokenAccountBalance(aliceTokenAAccount.address)).value.uiAmountString);
  console.log("Alice tokenB:", (await connection.getTokenAccountBalance(aliceTokenBAccount.address)).value.uiAmountString);
  console.log("Bob tokenA:", (await connection.getTokenAccountBalance(bobTokenAAccount.address)).value.uiAmountString);
  console.log("Bob tokenB:", (await connection.getTokenAccountBalance(bobTokenBAccount.address)).value.uiAmountString);
}

main().catch((e) => {
  console.error(e);
  process.exit(1);
});

运行与预期结果

  • 运行后你会看到在同一笔交易中两笔转账成功执行,最后 Alice 的 tokenA 减少 10 且获得 20 个 tokenB,Bob 相反。若任一 signer(Alice 或 Bob)不对交易签名或余额不足,交易会回滚(失败)。

优点与限制

  • 优点:实现极其简单、延迟低、不需要部署程序;只要双方同时在线签名即可完成原子交换。

  • 限制:双方必须同时对同一笔交易签名;若一方离线或不信任签名流程,可采用 escrow 流程(方法 B)。

方法 B:使用 Anchor 程序(on-chain 合约)来执行原子交换(程序端执行 CPI)

适用场景:想在链上有一个可审计的交换合约,能在单笔交易里执行复杂的业务逻辑(例如价格检查、nonce、签名验证、手续费分成、以及后续扩展如买断/仲裁)且仍然保证原子性。

核心思想

编写一个 Anchor 程序 atomic_swap,其 swap 指令需要传入双方的 token accounts(source/destination),并要求两个参与者(ownerA、ownerB)都作为 signer 出现在 transaction 的 account metas 中。程序执行 CPI 调用 token::transfer 两次,token program 会检查转账的 owner 是否在 transaction 的签名者列表里(是的话则允许),如果任一 transfer 因余额不足或缺签名失败,整个 instruction 会 panic,整笔交易回滚。

注意:程序能在执行时做更多检查(例如检查期望金额、双方的订单 id、deadline、签名 nonce 等),更适合生产环境。

下面给出完整 Anchor 程序(Rust)以及一个 TypeScript 客户端示例(使用 Anchor provider 调用),展示如何在同一笔交易中让两个用户签名并调用程序的 swap 指令。

Anchor 程序(完整)

  1. Anchor 项目初始化(本地)
anchor init atomic-swap-anchor --javascript
cd atomic-swap-anchor

编辑 Cargo.toml 与 Anchor.toml 按需(Anchor 默认生成即可)。

  1. Anchor 程序源码 — programs/atomic-swap-anchor/src/lib.rs

将下面内容替换或写入该文件:

use anchor_lang::prelude::*;
use anchor_spl::token::{self, Mint, TokenAccount, Transfer, Token};

declare_id!("ReplaceWithProgramIDWhenDeployedxxxxxxxxxxxxxxxxxxxx");

#[program]
pub mod atomic_swap_anchor {
    use super::*;

    /// swap: 程序会在 CPI 中执行两次 token transfer。
    /// preconditions:
    /// - owner_a 是 source_a 的 owner,并且为 signer(顶层交易签名)
    /// - owner_b 是 source_b 的 owner,并且为 signer
    /// - amount_a 和 amount_b 是预期转移数量
    pub fn swap(
        ctx: Context<Swap>,
        amount_a: u64,
        amount_b: u64
    ) -> Result<()> {
        // 做一些可选的检查
        // 比如可以验证 mint 是否符合预期(由客户端提供)
        // assert_eq!(ctx.accounts.mint_a.key(), some_expected_mint);

        // Transfer token A: source_a -> dest_a (owned by owner_b)
        let cpi_accounts_a = Transfer {
            from: ctx.accounts.source_a.to_account_info(),
            to: ctx.accounts.dest_a.to_account_info(),
            authority: ctx.accounts.owner_a.to_account_info(), // owner_a must be signer of tx
        };
        let cpi_program = ctx.accounts.token_program.to_account_info();
        let cpi_ctx_a = CpiContext::new(cpi_program.clone(), cpi_accounts_a);
        token::transfer(cpi_ctx_a, amount_a)?;

        // Transfer token B: source_b -> dest_b (owned by owner_a)
        let cpi_accounts_b = Transfer {
            from: ctx.accounts.source_b.to_account_info(),
            to: ctx.accounts.dest_b.to_account_info(),
            authority: ctx.accounts.owner_b.to_account_info(), // owner_b must be signer
        };
        let cpi_ctx_b = CpiContext::new(cpi_program, cpi_accounts_b);
        token::transfer(cpi_ctx_b, amount_b)?;

        Ok(())
    }
}

#[derive(Accounts)]
pub struct Swap<'info> {
    /// owner A: must be signer in the top-level transaction
    #[account(mut)]
    pub owner_a: Signer<'
info>,

    /// owner B: must be signer
    #[account(mut)]
    pub owner_b: Signer<'info>,

    /// source token account owned by owner_a (token A)
    #[account(mut, has_one = owner_a)]
    pub source_a: Account<'
info, TokenAccount>,

    /// destination token account for token A (owned by owner_b)
    #[account(mut, has_one = owner_b)]
    pub dest_a: Account<'info, TokenAccount>,

    /// source token account owned by owner_b (token B)
    #[account(mut, has_one = owner_b)]
    pub source_b: Account<'
info, TokenAccount>,

    /// destination token account for token B (owned by owner_a)
    #[account(mut, has_one = owner_a)]
    pub dest_b: Account<'info, TokenAccount>,

    /// SPL Token program
    pub token_program: Program<'
info, Token>,
}

说明:

  • 上面 Swap 账户结构通过 has_one 强制 source_a.owner == owner_a 等关系,Anchor 会在运行时自动检查。

  • owner_a 与 owner_b 都被声明为 Signer,这意味着调用该 instruction 的顶层交易必须包含这两个签名(实现了安全且原子的双向转移)。

  • 程序通过 CPI 调用 token program 的 transfer;token program 会检查传入的 authority 是否为 signer(而 outer tx 的签名会被 token program 检查通过),若缺签或余额不足则会 panic —— 从而回滚整个 instruction/transaction。

  1. 构建与部署(本地 devnet 或本地 validator)
  • 在项目根运行:anchor build

  • 配置 Anchor.toml network 为 devnet,或者用 anchor deploy --provider.cluster devnet(需要你已配置好 wallet 与网络)。

  • 部署后把 declare_id! 的 program id 替换为部署后的 program id。

生产建议:在部署前务必进行单元测试与安全审计。

客户端(TypeScript)示例:如何在单笔交易中让两个用户签名并调用程序

假设程序已部署且我们有 programId,下面演示如何使用 @project-serum/anchor 或直接用 @solana/web3.js 构建 transaction 并让 owner_a 与 owner_b 都签名。

安装(客户端)

npm i @project-serum/anchor @solana/web3.js @solana/spl-token

swap_via_anchor.ts(客户端示例)

import * as anchor from "@project-serum/anchor";
import { Keypair, SystemProgram, Transaction } from "@solana/web3.js";
import { TOKEN_PROGRAM_ID, getOrCreateAssociatedTokenAccount, createMint, mintTo } from "@solana/spl-token";

async functionmain() {
  // Provider & connection (Devnet)
  const provider = anchor.AnchorProvider.env(); // 需设置 env 或手动构造 provider
  anchor.setProvider(provider);
  const connection = provider.connection;

  // Load program (IDL generated by anchor build)
  const idl = JSON.parse(require("fs").readFileSync("./target/idl/atomic_swap_anchor.json""utf8"));
  const programId = new anchor.web3.PublicKey("PUT_YOUR_DEPLOYED_PROGRAM_ID_HERE");
  const program = new anchor.Program(idl, programId, provider);

  // Create two keypairs (ownerA, ownerB) — in production, they are external wallets
  const ownerA = Keypair.generate();
  const ownerB = Keypair.generate();

  // Airdrop both
  await connection.requestAirdrop(ownerA.publicKey, 2e9);
  await connection.requestAirdrop(ownerB.publicKey, 2e9);
  await new Promise((r) => setTimeout(r, 5000));

  // Create token mints and ATAs & mint balances (similar于方法A)
  const decimals = 6;
  const payer = provider.wallet.payer as Keypair;
  const mintA = await createMint(connection, payer, payer.publicKey, null, decimals);
  const mintB = await createMint(connection, payer, payer.publicKey, null, decimals);

  const ownerA_tokenA = await getOrCreateAssociatedTokenAccount(connection, payer, mintA, ownerA.publicKey);
  const ownerA_tokenB = await getOrCreateAssociatedTokenAccount(connection, payer, mintB, ownerA.publicKey);
  const ownerB_tokenA = await getOrCreateAssociatedTokenAccount(connection, payer, mintA, ownerB.publicKey);
  const ownerB_tokenB = await getOrCreateAssociatedTokenAccount(connection, payer, mintB, ownerB.publicKey);

  // Mint A: ownerA gets 100 tokenA; ownerB gets 200 tokenB
  await mintTo(connection, payer, mintA, ownerA_tokenA.address, payer, 100 * 10 ** decimals);
  await mintTo(connection, payer, mintB, ownerB_tokenB.address, payer, 200 * 10 ** decimals);

  // Now build the anchor instruction (program.rpc.swap)
  // Anchor expects the accounts layout matching the program's Swap context
  const amountA = 10 * 10 ** decimals; // ownerA -> ownerB
  const amountB = 20 * 10 ** decimals; // ownerB -> ownerA

  // Build transaction but require ownerA & ownerB to sign
  const tx = await program.rpc.swap(
    new anchor.BN(amountA),
    new anchor.BN(amountB),
    {
      accounts: {
        ownerA: ownerA.publicKey,
        ownerB: ownerB.publicKey,
        sourceA: ownerA_tokenA.address,
        destA: ownerB_tokenA.address,
        sourceB: ownerB_tokenB.address,
        destB: ownerA_tokenB.address,
        tokenProgram: TOKEN_PROGRAM_ID,
      },
      signers: [ownerA, ownerB], // Anchor will attach signatures from these signers
      instructions: [],
    }
  );

  console.log("Swap txid:", tx);
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});

要点说明:

  • 在上面 program.rpc.swap(..., { signers: [ownerA, ownerB] }) 中,Anchor 会把 ownerA 与 ownerB 放到交易的签名位,使得当程序执行 CPI 时,token program 能识别这些账户为签名者并允许 transfer。

  • 若任何一方没有签名或余额不足,CPI token::transfer 将失败,从而导致程序 swap 返回错误,整笔交易回滚(保证原子性)。

  • Anchor 的 has_one 在运行时会对账户关系做验证,增强安全性。


图片
扫描二维码关注我们

本公众号发布的内容除特别标明外版权归原作者所有。若涉及版权问题,请联系我们。所有信息及评论区内容仅供参考,请读者自行判断信息真伪,不构成任何投资建议。据此产生的任何损失,本公众号概不负责,亦不负任何法律责任。



【声明】内容源于网络
0
0
链块高度
全栈式区块链技术解决方案供应商和服务商,致力于为客户提供优质的开发设计方案。成功落地多款成熟链游、NFT数藏平台、交易所Swap、Dapp、元宇宙、企业信息化、数据管理备份恢复迁移容灾等优质产品。
内容 16
粉丝 0
链块高度 全栈式区块链技术解决方案供应商和服务商,致力于为客户提供优质的开发设计方案。成功落地多款成熟链游、NFT数藏平台、交易所Swap、Dapp、元宇宙、企业信息化、数据管理备份恢复迁移容灾等优质产品。
总阅读26
粉丝0
内容16