您正在查看: 2025年12月

更换Git子模块的仓库地址

更换子模块地址

  1. 修改 .gitmodules
    [submodule "libs/foo"]
     path = libs/foo
     url  = https://github.com/old-org/foo.git

    改成:

    [submodule "libs/foo"]
     path = libs/foo
     url  = https://github.com/new-org/foo.git
  2. 同步配置
    git submodule sync
  3. 更新子模块
    git submodule update --init --recursive
  4. 更新仓库
    git add .gitmodules
    git commit -m "chore: update submodule foo url"
    git push

更换主仓库地址

  1. 查看当前远端
    git remote -v

    你会看到类似:

    origin  https://github.com/old-org/project.git (fetch)
    origin  https://github.com/old-org/project.git (push)
  2. 修改 origin 地址
    git remote set-url origin https://github.com/new-org/project.git

    验证

    git remote -v
  3. 推送到新组织(第一次建议加 -u)
    git push -u origin main
    # 或
    git push -u origin master

OpenVPN 管理器:面向企业 VPN 管理的完整开源解决方案

管理整个组织内的 VPN 用户不应该再像 2005 年那样了。不再需要使用包含凭据的电子表格、手动配置文件,也不再需要费心去了解“现在谁在线?”。

OpenVPN Manager——一个开源的 Web 应用程序,它为您的 VPN 基础架构带来现代化的用户管理;同时发布的还有OpenVPN Client——一个配套工具,它可以将所有内容无缝集成。

问题

如果你曾经为团队管理过 OpenVPN 服务器,你就会了解这些痛点:

  • 用户管理一片混乱——添加用户意味着生成证书、编辑配置,还要祈祷别搞砸了什么。
  • 完全看不到任何信息——谁在线?带宽使用情况如何?连接历史记录?祝你好运,希望能找到答案。
  • 访问控制难题——工程部门和人力资源部门需要不同的网络访问权限,但 OpenVPN 并没有简化这一过程。
  • 审计要求——当安全部门询问“谁在何时访问了什么?”时,您可能需要花费数小时翻阅日志。
  • 承包商访问权限——本应过期但从未过期的临时用户。

解决方案:两个项目,一个完整系统

OpenVPN 管理器(Web 应用程序)

整个系统的核心。一个基于网页的管理面板,您可以在这里管理所有事项:

基于角色的访问控制的用户管理

三个角色确保一切井然有序:

  • 用户——可以查看和编辑自己的个人资料
  • 管理员——可以创建和管理团队中的用户
  • 管理员——拥有所有权限

非常适合团队负责人需要管理团队的 VPN 访问权限,而又不想麻烦 IT 部门的组织。


基于组的网络访问

定义用户组(例如工程组、人力资源组、财务组),并为每个用户分配网络段。用户继承其所属用户的访问权限。简单、可扩展且可审计。


VPN IP 自动分配

只需配置一次 VPN 网络范围,系统就会自动为新用户分配下一个可用的 IP 地址。从此告别 IP 地址冲突和手动追踪。

会话跟踪与审核

最后,了解您的 VPN 信息:

  • 实时连接状态
  • 用户带宽使用情况
  • 完整的连接历史记录
  • 所有变更的完整审计跟踪


限时访问

需要授予承包商两周的访问权限?设置日期valid_from即可valid_to。访问权限会自动过期。再也不用担心忘记登录了。

OpenVPN客户端(集成脚本)

将你的 OpenVPN 服务器连接到管理器的 Go 二进制文件:

┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ 
│ OpenVPN 服务器 │─────────────▶│ OpenVPN 客户端 │────▶│ OpenVPN 管理器
│ │ │ │ (Go 二进制文件)                           │ │ (Web 应用) │ 
└─────────────────┘ └───────────────────┘ └───────────────────┘ 
                               │ 
                               ▼ 
                        ┌───────────────────┐ 
                        │ nftables/iptables 
                        │ │ (防火墙) │ 
                        └─────────────────┘

四个二进制文件处理所有事情:

BinaryPurpose 功能openvpn-login包括:根据 API 验证用户凭据openvpn-connect;分配 IP 地址;根据组成员身份推送路由openvpn-disconnect;记录会话结束和流量统计信息;openvpn-firewall为每个用户生成 nftables/iptables 规则。

完整安装指南

步骤 1:安装 OpenVPN 管理器

选项 A:DEB 软件包(Debian/Ubuntu)

VERSION="1.0.1" 
wget https://github.com/tldr-it-stepankutaj/openvpn-mng/releases/download/v${VERSION}/openvpn-mng_${VERSION}_linux_amd64.deb 
sudo dpkg -i openvpn-mng_${VERSION}_linux_amd64.deb

选项 B:RPM 软件包(RHEL/AlmaLinux/Rocky)

VERSION="1.0.1" 
wget https://github.com/tldr-it-stepankutaj/openvpn-mng/releases/download/v${VERSION}/openvpn-mng_${VERSION}_linux_amd64.rpm 
sudo dnf install ./openvpn-mng_${VERSION}_linux_amd64.rpm

选项 C:Docker

docker pull tldrit/openvpn-mng:latest

步骤 2:配置数据库

# PostgreSQL 
sudo -u postgres psql 
CREATE USER openvpn WITH PASSWORD 'your-secure-password'; 
CREATE DATABASE openvpn_mng OWNER openvpn; 
\q

步骤 3:配置 OpenVPN 管理器

# 生成密钥
JWT_SECRET=$(openssl rand -hex 32) 
VPN_TOKEN=$(openssl rand -hex 32)
echo "JWT_SECRET: $JWT_SECRET" 
echo "VPN_TOKEN: $VPN_TOKEN"
# 编辑配置文件
sudo nano /etc/openvpn-mng/config.yaml

关键设置:

database:
  type: "postgres"
  host: "localhost"
  password: "your-db-password"
  database: "openvpn_mng"
auth:
  jwt_secret: "your-generated-jwt-secret"
api:
  vpn_token: "your-generated-vpn-token"
vpn:
  network: "10.90.90.0/24"
  server_ip: "10.90.90.1"

步骤 4:启动 OpenVPN 管理器

sudo systemctl enable --now openvpn-mng.service
# 验证
curl http://localhost:8080/

默认登录名:admin/ admin123— 请立即更改!

步骤 5:安装 OpenVPN 客户端

选项 A:DEB 软件包(Debian/Ubuntu)

VERSION="1.0.0" 
wget https://github.com/tldr-it-stepankutaj/openvpn-client/releases/download/v${VERSION}/openvpn-client_${VERSION}_linux_amd64.deb 
sudo dpkg -i openvpn-client_${VERSION}_linux_amd64.deb

选项 B:RPM 软件包(RHEL/AlmaLinux/Rocky)

VERSION="1.0.0" 
wget https://github.com/tldr-it-stepankutaj/openvpn-client/releases/download/v${VERSION}/openvpn-client_${VERSION}_linux_amd64.rpm 
sudo dnf install ./openvpn-client_${VERSION}_linux_amd64.rpm

选项 C:从源代码构建

git clone https://github.com/tldr-it-stepankutaj/openvpn-client.git 
cd openvpn-client 
make build-linux 
sudo make install

步骤 6:配置 OpenVPN 客户端

sudo mkdir -p /etc/openvpn/client 
sudo nano /etc/openvpn/client/config.yaml
api:
  base_url: "http://127.0.0.1:8080"
  token: "your-vpn-token-from-step-3"
  timeout: 10s
openvpn:
  session_dir: "/var/run/openvpn"
firewall:
  type: "nftables"
  nftables:
    rules_file: "/etc/nftables.d/vpn-users.nft"
    reload_command: "/usr/sbin/nft -f /etc/sysconfig/nftables.conf"

步骤 7:配置 OpenVPN 服务器

添加到您的 OpenVPN 服务器配置中:

# VPN network (must match OpenVPN Manager config)
server 10.90.90.0 255.255.255.0
topology subnet
# Authentication via OpenVPN Manager
username-as-common-name
auth-user-pass-verify /usr/bin/openvpn-login via-file
client-connect /usr/bin/openvpn-connect
client-disconnect /usr/bin/openvpn-disconnect
script-security 2

步骤 8:设置防火墙规则定时任务

# 创建初始规则
sudo mkdir -p /etc/nftables.d 
sudo touch /etc/nftables.d/vpn-users.nft 
sudo /usr/bin/openvpn-firewall
# 添加 cron 任务
echo "*/5 * * * * root /usr/bin/openvpn-firewall >> /var/log/openvpn-firewall.log 2>&1" | sudo tee /etc/cron.d/openvpn-firewall

步骤 9:重启 OpenVPN

sudo systemctl restart openvpn@server

这一切是如何协同运作的

  1. 用户使用用户名和密码连接
  2. openvpn-login使用 Manager API 验证凭据
  3. openvpn-connect获取用户的组和网络,然后推送路由
  4. 防火墙规则根据用户的组成员身份限制流量
  5. 会话在管理器数据库中进行跟踪。
  6. 管理员可在网页界面查看实时连接。
  7. openvpn-disconnect 会在用户断开连接时记录流量统计信息。

技术栈

OpenVPN 管理器:

  • Go + Gin 框架
  • GORM(PostgreSQL/MySQL)
  • JWT 身份验证
  • Bootstrap 5 Web UI

OpenVPN客户端:

  • Go
  • nftables/iptables 支持
  • 结构化日志记录(slog)

两者都编译成单个二进制文件,可以部署到任何地方。

接下来是什么?

计划功能:

  • LDAP/Active Directory 集成
  • 双因素身份验证
  • WireGuard 支持
  • 普罗米修斯指标
  • Terraform 提供程序

开始使用

OpenVPN 管理器:

OpenVPN客户端:

这两个项目均采用 Apache 2.0 许可协议。欢迎贡献代码!

如果您厌倦了传统的 VPN 用户管理方式,不妨试试这个方法。如果您觉得它有用,请在 GitHub 上点个星标,非常感谢!

原文:https://medium.com/@stepan_95301/openvpn-manager-a-complete-open-source-solution-for-enterprise-vpn-management-fe214b8f4627

MPCium 实测

部署

  1. 编译部署

git clone https://github.com/fystack/mpcium.git
cd mpcium
source /etc/profile
go env -w GOPRIVATE=github.com/bcskill/*
go mod tidy
make

可用命令

  • mpcium:启动 MPCium 节点
  • mpcium-cli:用于对等方、身份和发起方配置的 CLI 实用程序
go build ./cmd/mpcium
go build ./cmd/mpcium-cli
  1. 启动NATS 和 Consul

cd mpcium
docker compose up -d

NATS:高性能、轻量级的 消息系统(消息队列 / 消息总线),主要用于服务之间的异步通信

Service A ───► NATS ───► Service B

​ ▲

​ └─► Service C(也可同时收到消息)

Consul:HashiCorp 开源的一个用于服务网络的工具,主要用于 服务发现、配置管理、健康检查,也支持 分布式 Key-Value 存储服务网格功能

[Service A] <---> [Consul Agent] <---> [Consul Server] <---> [Service B info]

  1. 部署节点

3.1 创建节点配置

cd ../
mkdir test-node
cd test-node
mpcium-cli generate-peers -n 3
cp ../mpcium/config.yaml.template ./config.yaml

编辑badger_password 为16,24,32 位密码

3.2 注册链节点

mpcium-cli register-peers

3.3 生成启动器

mpcium-cli generate-initiator

从生成的event_initiator.identity.json 中复制public_key, 更新到 config.yaml中的event_initiator_pubkey

3.4 创建目录

mkdir node{0..2}
for dir in node{0..2}; do cp config.yaml peers.json "$dir/"; done

3.5 创建身份

cd node0
mpcium-cli generate-identity --node node0
cd ../node1
mpcium-cli generate-identity --node node1
cd ../node2
mpcium-cli generate-identity --node node2
cd ../

cp node0/identity/node0_identity.json node1/identity/node0_identity.json
cp node0/identity/node0_identity.json node2/identity/node0_identity.json
cp node1/identity/node1_identity.json node0/identity/node1_identity.json
cp node1/identity/node1_identity.json node2/identity/node1_identity.json
cp node2/identity/node2_identity.json node0/identity/node2_identity.json
cp node2/identity/node2_identity.json node1/identity/node2_identity.json

3.6 启动节点

cd node0
mpcium start -n node0
cd node1
mpcium start -n node1
cd node2
mpcium start -n node2

实际测试

https://github.com/fystack/mpcium-client-ts

event_initiator.key

f81851f905abc51de2618ddb7f75dde2a422dd0f6f3abb0d51c850287ecac900

创建钱包

import { connect } from "nats";
import { KeygenSuccessEvent, MpciumClient } from "../src";
import { computeAddress, hexlify } from "ethers";
import base58 from "bs58";
import * as fs from "fs";
import * as path from "path";
import { v4 } from "uuid";

async function main() {
  const args = process.argv.slice(2);
  const nIndex = args.indexOf("-n");
  const walletCount = nIndex !== -1 ? parseInt(args[nIndex + 1]) || 1 : 1;

  const nc = await connect({ servers: "nats://127.0.0.1:4222" }).catch(
    (err) => {
      console.error(`Failed to connect to NATS: ${err.message}`);
      process.exit(1);
    }
  );
  console.log(`Connected to NATS at ${nc.getServer()}`);

  const mpcClient = await MpciumClient.create({
    nc: nc,
    keyPath: "./event_initiator.key",
    // password: "your-password-here",
  });

  const walletsPath = path.resolve("./wallets.json");
  let wallets: Record<string, KeygenSuccessEvent> = {};
  if (fs.existsSync(walletsPath)) {
    try {
      wallets = JSON.parse(fs.readFileSync(walletsPath, "utf8"));
    } catch (error) {
      console.warn(`Could not read wallets file: ${error.message}`);
    }
  }

  let remaining = walletCount;

  mpcClient.onWalletCreationResult((event: KeygenSuccessEvent) => {
    const timestamp = new Date().toISOString();
    console.log(`${timestamp} Received wallet creation result:`, event);

    if (event.eddsa_pub_key) {
      const pubKeyBytes = Buffer.from(event.eddsa_pub_key, "base64");
      const solanaAddress = base58.encode(pubKeyBytes);
      console.log(`Solana wallet address: ${solanaAddress}`);
    }

    if (event.ecdsa_pub_key) {
      const pubKeyBytes = Buffer.from(event.ecdsa_pub_key, "base64");
      const uncompressedKey =
        pubKeyBytes.length === 65
          ? pubKeyBytes
          : Buffer.concat([Buffer.from([0x04]), pubKeyBytes]);
      const ethAddress = computeAddress(hexlify(uncompressedKey));
      console.log(`Ethereum wallet address: ${ethAddress}`);
    }

    wallets[event.wallet_id] = event;
    fs.writeFileSync(walletsPath, JSON.stringify(wallets, null, 2));
    console.log(`Wallet saved to wallets.json with ID: ${event.wallet_id}`);

    remaining -= 1;
    if (remaining === 0) {
      console.log("All wallets generated.");
    }
  });

  try {
    for (let i = 0; i < walletCount; i++) {
      const timestamp = new Date().toISOString();
      const walletID = await mpcClient.createWallet(`${v4()}:${i}`);
      console.log(
        `${timestamp} CreateWallet sent #${
          i + 1
        }, awaiting result... walletID: ${walletID}`
      );
    }

    const shutdown = async () => {
      console.log("Cleaning up...");
      await mpcClient.cleanup();
      await nc.drain();
      process.exit(0);
    };

    process.on("SIGINT", shutdown);
  } catch (error) {
    console.error("Error:", error);
    await mpcClient.cleanup();
    await nc.drain();
    process.exit(1);
  }
}

main().catch(console.error);
/mpcium-client-ts# ts-node ./examples/generate.ts 
Connected to NATS at 127.0.0.1:4222
Subscribed to wallet creation results (consume mode)
  result_type: 'success',
  error_reason: '',
  error_code: ''
}
Solana wallet address: Cmroym1zhiv6Q3DaHSRQv3CJBFnBqm4dtLkYnsAqbQ2c
Ethereum wallet address: 0xef049bd27aCD860D7A157e492378bf54eE9671B9
Wallet saved to wallets.json with ID: b7b3cb5e-fbea-4785-86c7-f67c3562ddfc:0

签名发送交易

测试地址,忽略手续费不足问题

import { connect } from "nats";
import { MpciumClient, KeyType, SigningResultEvent } from "../src";
import { ethers } from "ethers";
import { SigningResultType } from "../src/types";
import * as fs from "fs";
import * as path from "path";

// The wallet ID should be provided via command line argument
const walletId = "b7b3cb5e-fbea-4785-86c7-f67c3562ddfc:0";

// Destination wallet to send ETH to
const DESTINATION_WALLET = "0x742d35Cc6634C0532925a3b844Bc454e4438f44e";

// Amount to send in ETH
const AMOUNT_TO_SEND = "0.0001"; // 0.001 ETH

// Function to load wallet from wallets.json
function loadWallet(walletId: string) {
  const walletsPath = path.resolve("./wallets.json");
  try {
    if (fs.existsSync(walletsPath)) {
      const wallets = JSON.parse(fs.readFileSync(walletsPath, "utf8"));
      if (wallets[walletId]) {
        return wallets[walletId];
      }
      throw new Error(`Wallet with ID ${walletId} not found in wallets.json`);
    } else {
      throw new Error("wallets.json file not found");
    }
  } catch (error) {
    console.error(`Failed to load wallet: ${error.message}`);
    process.exit(1);
  }
}

async function main() {
  console.log(`Using wallet ID: ${walletId}`);

  // First, establish NATS connection separately
  const nc = await connect({ servers: "nats://127.0.0.1:4222" }).catch(
    (err) => {
      console.error(`Failed to connect to NATS: ${err.message}`);
      process.exit(1);
    }
  );
  console.log(`Connected to NATS at ${nc.getServer()}`);

  // Create client with key path
  const mpcClient = await MpciumClient.create({
    nc: nc,
    keyPath: "./event_initiator.key",
    // password: "your-password-here", // Required for .age encrypted keys
  });

  try {
    // Connect to Ethereum testnet (Sepolia)
    const provider = new ethers.JsonRpcProvider(
      "https://eth-sepolia.public.blastapi.io"
    );
    console.log("Connected to Ethereum Sepolia testnet");

    // Get the wallet's public key/address
    const ethAddress = await getEthAddressForWallet(walletId);
    const fromAddress = ethAddress;

    console.log(`Sender account: ${fromAddress}`);
    console.log(`Destination account: ${DESTINATION_WALLET}`);
    console.log(`Amount to send: ${AMOUNT_TO_SEND} ETH`);

    // Get the current nonce for the sender address
    const nonce = await provider.getTransactionCount(fromAddress);

    // Get the current gas price
    const feeData = await provider.getFeeData();

    console.log("feeData:", feeData);

    // Create an Ethereum transaction
    const transaction = {
      to: DESTINATION_WALLET,
      value: ethers.parseEther(AMOUNT_TO_SEND),
      gasLimit: 21000, // Standard gas limit for ETH transfers
      maxFeePerGas: feeData.maxFeePerGas,
      maxPriorityFeePerGas: feeData.maxPriorityFeePerGas,
      nonce: nonce,
      type: 2, // EIP-1559 transaction
      chainId: 11155111, // Sepolia chain ID
    };

    // Calculate the transaction hash (this is what needs to be signed)
    const unsignedTx = ethers.Transaction.from(transaction);
    const txHash = unsignedTx.unsignedHash;
    const txHashHex = txHash.substring(2); // Remove '0x' prefix

    console.log(`Transaction hash: ${txHash}`);

    // Subscribe to signing results
    let signatureReceived = false;
    mpcClient.onSignResult((event: SigningResultEvent) => {
      console.log("Received signing result:", event);
      signatureReceived = true;

      if (event.result_type === SigningResultType.Success) {
        processSuccessfulSignature(event);
      } else {
        console.error(`Signing failed: ${event.error_message}`);
      }
    });

    // Process a successful signature
    function processSuccessfulSignature(event: SigningResultEvent) {
      try {
        // For ECDSA Ethereum signatures, we need the r, s, and v (recovery) values
        if (!event.r || !event.s || event.signature_recovery === null) {
          console.error("Missing signature components in result:", event);
          return;
        }

        // Convert from base64 to hex strings
        const r = "0x" + Buffer.from(event.r, "base64").toString("hex");
        const s = "0x" + Buffer.from(event.s, "base64").toString("hex");

        // Decode signature_recovery from base64 to a number
        const recoveryBuffer = Buffer.from(event.signature_recovery, "base64");
        const v = recoveryBuffer[0]; // Get the first byte as the recovery value

        console.log(`Signature components - r: ${r}, s: ${s}, v: ${v}`);

        // Create a signed transaction
        const signedTx = ethers.Transaction.from({
          ...transaction,
          signature: { r, s, v },
        });

        // Verify signature
        const recoveredAddress = signedTx.from;
        console.log(`Recovered signer: ${recoveredAddress}`);
        if (!recoveredAddress) {
          console.error("Signature verification failed!");
          return;
        }

        if (recoveredAddress.toLowerCase() !== fromAddress.toLowerCase()) {
          console.error(
            "Signature verification failed! Addresses don't match."
          );
          return;
        }

        console.log("Signature verification successful!");
        broadcastTransaction(signedTx.serialized);
      } catch (error) {
        console.error("Error processing signature:", error);
        if (error instanceof Error) {
          console.error(error.stack);
        }
      }
    }

    // Broadcast the transaction to the network
    function broadcastTransaction(signedTxHex: string) {
      provider
        .broadcastTransaction(signedTxHex)
        .then((tx) => {
          console.log(`Transaction sent! Transaction hash: ${tx.hash}`);
          console.log(
            `View transaction: https://sepolia.etherscan.io/tx/${tx.hash}`
          );
        })
        .catch((err) => {
          console.error("Error broadcasting transaction:", err);
        });
    }

    // Send the transaction hash for signing, not the serialized transaction
    const txId = await mpcClient.signTransaction({
      walletId: walletId,
      keyType: KeyType.Secp256k1,
      networkInternalCode: "ethereum:sepolia",
      tx: Buffer.from(txHashHex, "hex").toString("base64"), // Convert hex to base64
    });

    console.log(`Signing request sent with txID: ${txId}`);

    // Wait for the result
    await new Promise((resolve) => {
      const checkInterval = setInterval(() => {
        if (signatureReceived) {
          clearInterval(checkInterval);
          resolve(null);
        }
      }, 1000);
    });

    // Keep the process running to allow time for transaction confirmation
    await new Promise((resolve) => setTimeout(resolve, 5000));

    console.log("Cleaning up...");
    await mpcClient.cleanup();
    await nc.drain();
  } catch (error) {
    console.error("Error:", error);
    await mpcClient.cleanup();
    await nc.drain();
    process.exit(1);
  }
}

// Helper function to get a wallet's Ethereum address
async function getEthAddressForWallet(walletId: string): Promise<string> {
  // Load wallet from wallets.json
  const wallet = loadWallet(walletId);

  if (wallet && wallet.ecdsa_pub_key) {
    // Convert base64 public key to Ethereum address
    const pubKeyBuffer = Buffer.from(wallet.ecdsa_pub_key, "base64");
    // Convert the buffer to hex string with "0x" prefix
    const pubKeyHex = "0x" + pubKeyBuffer.toString("hex");
    // Ethereum addresses are derived from the keccak256 hash of the uncompressed public key
    const address = ethers.computeAddress(pubKeyHex);
    return address;
  }

  throw new Error(`Wallet with ID ${walletId} has no ECDSA public key`);
}

// Run the example
main().catch(console.error);
/mpcium-client-ts# ts-node ./examples/sign-eth.ts
Using wallet ID: b7b3cb5e-fbea-4785-86c7-f67c3562ddfc:0
Connected to NATS at 127.0.0.1:4222
Connected to Ethereum Sepolia testnet
Sender account: 0xef049bd27aCD860D7A157e492378bf54eE9671B9
Destination account: 0x742d35Cc6634C0532925a3b844Bc454e4438f44e
Amount to send: 0.0001 ETH
feeData: FeeData {
  gasPrice: 14533302n,
  maxFeePerGas: 28066604n,
  maxPriorityFeePerGas: 1000000n
}
Transaction hash: 0x9c7835d42ef9643c110d5ddf038fba76a580c93a650242a1d66f5d74333bf3c7
Subscribed to signing results (consume mode)
SignTransaction request sent via JetStream for txID: c8354d11-aba5-46e8-88c2-c846b1fdac51
Signing request sent with txID: c8354d11-aba5-46e8-88c2-c846b1fdac51
Received signing result: {
  result_type: 'success',
  error_code: '',
  error_reason: '',
  is_timeout: false,
  network_internal_code: 'ethereum:sepolia',
  wallet_id: 'b7b3cb5e-fbea-4785-86c7-f67c3562ddfc:0',
  tx_id: 'c8354d11-aba5-46e8-88c2-c846b1fdac51',
  r: 'pylQvdMr+Z7N+9ADAdruVN7MTqysS0LNvVERgEx4Ktw=',
  s: 'ZA4LMVBf3Di9E35C3vph3ZWUvH4GHNnRTBb7aKYHFuc=',
  signature_recovery: 'AA==',
  signature: null
}
Signature components - r: 0xa72950bdd32bf99ecdfbd00301daee54decc4eacac4b42cdbd5111804c782adc, s: 0x640e0b31505fdc38bd137e42defa61dd9594bc7e061cd9d14c16fb68a60716e7, v: 0
Recovered signer: 0xef049bd27aCD860D7A157e492378bf54eE9671B9
Signature verification successful!
Error broadcasting transaction: Error: insufficient funds for intrinsic transaction cost (transaction="0x02f87283aa36a780830f42408401ac432c82520894742d35cc6634c0532925a3b844bc454e4438f44e865af3107a400080c080a0a72950bdd32bf99ecdfbd00
301daee54decc4eacac4b42cdbd5111804c782adca0640e0b31505fdc38bd137e42defa61dd9594bc7e061cd9d14c16fb68a60716e7", info={ "error": { "code": -32003, "message": "insufficient funds for gas * price + value: have 0 want 100589398684000" } }, code=INSUFFICIENT_FUNDS, version=6.13.7)
    at makeError (/mnt/d/github/bcskill/code/MPC/mpcium-client-ts/node_modules/ethers/src.ts/utils/errors.ts:694:21)
    at JsonRpcProvider.getRpcError (/mnt/d/github/bcskill/code/MPC/mpcium-client-ts/node_modules/ethers/src.ts/providers/provider-jsonrpc.ts:1025:33)
    at /mnt/d/github/bcskill/code/MPC/mpcium-client-ts/node_modules/ethers/src.ts/providers/provider-jsonrpc.ts:563:45
    at processTicksAndRejections (node:internal/process/task_queues:95:5) {
  code: 'INSUFFICIENT_FUNDS',
  transaction: '0x02f87283aa36a780830f42408401ac432c82520894742d35cc6634c0532925a3b844bc454e4438f44e865af3107a400080c080a0a72950bdd32bf99ecdfbd00301daee54decc4eacac4b42cdbd5111804c782adca0640e0b31505fdc38bd137e42defa61dd9594bc7e061cd9d14c16fb68a60716e7',
  info: {
    error: {
      code: -32003,
      message: 'insufficient funds for gas * price + value: have 0 want 100589398684000'
    }
  },
  shortMessage: 'insufficient funds for intrinsic transaction cost'
}
Cleaning up...
Cleaned up all subscriptions

多钱包测试

Subscribed to wallet creation results (consume mode)
CreateWallet request sent via JetStream for wallet: bbcd5019-2e7d-46aa-a1e8-5bc14371bc57:0
2025-07-14T12:57:13.424Z CreateWallet sent #1, awaiting result... walletID: bbcd5019-2e7d-46aa-a1e8-5bc14371bc57:0
2025-07-14T12:57:17.181Z Received wallet creation result: {
  wallet_id: 'bbcd5019-2e7d-46aa-a1e8-5bc14371bc57:0',
  ecdsa_pub_key: '2+ZZj1XCFR/OGWk9g1EMYZFyXfDNaNHCf+egn1oTo6DvR6kQ6tOBHK0oZkCL6QNsjmfi14tQN71W5n7OOXLSBg==',
  eddsa_pub_key: 's8rOVjh6dMwmpXqRDRYSyOBenY/uaHBOUvumU2QFuBE=',
  result_type: 'success',
  error_reason: '',
  error_code: ''
}
Solana wallet address: D6qLkdfZLXjHhSLAJ38UHjkYmBHU4Ac8S1oQKUDJZ2Gx
Ethereum wallet address: 0xCfcec2Beed19a40A663494396fB73042f2049dB1
Wallet saved to wallets.json with ID: bbcd5019-2e7d-46aa-a1e8-5bc14371bc57:0

测试通过

异常测试

使用相同walletID

WRN Keygen session error error="Key already exists: 81418adb-fb32-4e17-9b85-8ae207aa9cb8:0" context="Failed to create ECDSA key generation session" errorCode=ERROR_KEY_ALREADY_EXISTS walletID=81418adb-fb32-4e17-9b85-8ae207aa9cb8:0

https://github.com/fystack/mpcium/issues/56

常见问题

创建地址无返回

问题排查

nats consumer info
> Select a Stream mpc
> Select a Consumer mpc_keygen_result

查看 Waiting Pulls: 3 of maximum 512 当前有几个客户端连接

移除所有监听列表

nats consumer rm mpc mpc_keygen_result