@coin-voyage/crypto 2.2.3 → 2.3.1-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,4 @@
1
+ import { getEvmPaymentData } from "@coin-voyage/shared/common";
1
2
  import { estimateFeesPerGas } from "viem/actions";
2
3
  import { useConfig, useSendTransaction, useWriteContract } from "wagmi";
3
4
  export function useEVMTransaction() {
@@ -5,14 +6,31 @@ export function useEVMTransaction() {
5
6
  const { sendTransactionAsync } = useSendTransaction();
6
7
  const config = useConfig();
7
8
  const execute = async (params) => {
8
- const { chainId, toAddress, amount, token } = params;
9
- const value = BigInt(amount);
10
- const to = toAddress;
11
9
  const client = config.getClient();
12
10
  const gasEstimates = await estimateFeesPerGas(client, {
13
11
  chain: client.chain,
14
12
  type: "eip1559",
15
13
  });
14
+ const evmPaymentData = getEvmPaymentData(params.paymentData);
15
+ if (evmPaymentData) {
16
+ const tx = await sendTransactionAsync({
17
+ to: evmPaymentData.to,
18
+ data: evmPaymentData.data,
19
+ value: BigInt(evmPaymentData.value),
20
+ chainId: evmPaymentData.chainId,
21
+ maxFeePerGas: evmPaymentData.maxFeePerGas ? BigInt(evmPaymentData.maxFeePerGas) : gasEstimates.maxFeePerGas,
22
+ maxPriorityFeePerGas: evmPaymentData.maxPriorityFeePerGas
23
+ ? BigInt(evmPaymentData.maxPriorityFeePerGas)
24
+ : gasEstimates.maxPriorityFeePerGas,
25
+ });
26
+ return tx;
27
+ }
28
+ const { chainId, to: toAddress, amount, token } = params;
29
+ if (!toAddress || amount == undefined) {
30
+ throw new Error("Missing EVM transfer target or amount");
31
+ }
32
+ const value = BigInt(amount);
33
+ const to = toAddress;
16
34
  if (!token) {
17
35
  const tx = await sendTransactionAsync({
18
36
  to,
@@ -1,7 +1,10 @@
1
+ import { getSolanaPaymentData } from "@coin-voyage/shared/common";
2
+ import { hex } from "@scure/base";
1
3
  import { createTransferCheckedInstruction, getAssociatedTokenAddress } from "@solana/spl-token";
2
4
  import { useConnection, useWallet } from "@solana/wallet-adapter-react";
3
- import { ComputeBudgetProgram, PublicKey, SystemProgram, Transaction, TransactionInstruction, } from "@solana/web3.js";
5
+ import { ComputeBudgetProgram, PublicKey, SystemProgram, Transaction, TransactionInstruction, VersionedTransaction, } from "@solana/web3.js";
4
6
  import { getOrCreateAssociatedTokenAccount } from "./get-or-create-ata";
7
+ const INVALID_TX_PAYLOAD_ERROR = "Invalid Solana transaction payload: expected Transaction, VersionedTransaction, Uint8Array, byte array, hex, or base64";
5
8
  export function useSolanaTransaction() {
6
9
  const { connection } = useConnection();
7
10
  const wallet = useWallet();
@@ -14,10 +17,22 @@ export function useSolanaTransaction() {
14
17
  throw new Error("Wallet not connected. Please reconnect your wallet and try again.");
15
18
  }
16
19
  }
17
- const { fromAddress, toAddress, amount, token } = params;
20
+ const preparedPayment = getSolanaPaymentData(params.paymentData);
21
+ if (preparedPayment) {
22
+ const { context: { slot: minContextSlot }, value: { lastValidBlockHeight }, } = await connection.getLatestBlockhashAndContext();
23
+ const transaction = deserializePreparedSolanaTransaction(preparedPayment.transaction);
24
+ const signature = await wallet.sendTransaction(transaction, connection, {
25
+ minContextSlot,
26
+ });
27
+ return waitForTransactionConfirmation(signature, connection, lastValidBlockHeight);
28
+ }
29
+ const { from, to, amount, token } = params;
30
+ if (!to || amount == undefined) {
31
+ throw new Error("Missing Solana transfer target or amount");
32
+ }
18
33
  const transaction = new Transaction();
19
- const senderPubKey = new PublicKey(fromAddress);
20
- const receiverPubKey = new PublicKey(toAddress);
34
+ const senderPubKey = new PublicKey(from);
35
+ const receiverPubKey = new PublicKey(to);
21
36
  const instructions = [
22
37
  ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 1000000 }),
23
38
  ];
@@ -56,6 +71,52 @@ export function useSolanaTransaction() {
56
71
  };
57
72
  return { execute };
58
73
  }
74
+ function toSerializedTransactionBytes(value) {
75
+ if (value instanceof Uint8Array)
76
+ return value;
77
+ if (Array.isArray(value)) {
78
+ if (!value.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) {
79
+ throw new Error("Invalid Solana transaction payload: array must contain byte values");
80
+ }
81
+ return Uint8Array.from(value);
82
+ }
83
+ if (typeof value !== "string") {
84
+ throw new Error(INVALID_TX_PAYLOAD_ERROR);
85
+ }
86
+ const normalized = value.trim();
87
+ if (!normalized) {
88
+ throw new Error("Invalid Solana transaction payload: empty string");
89
+ }
90
+ const hexValue = normalized.startsWith("0x") ? normalized.slice(2) : normalized;
91
+ const looksLikeHex = hexValue.length > 0 && hexValue.length % 2 === 0 && /^[0-9a-fA-F]+$/.test(hexValue);
92
+ if (looksLikeHex) {
93
+ return hex.decode(hexValue);
94
+ }
95
+ try {
96
+ return Uint8Array.from(atob(normalized), (char) => char.charCodeAt(0));
97
+ }
98
+ catch {
99
+ throw new Error(INVALID_TX_PAYLOAD_ERROR);
100
+ }
101
+ }
102
+ function deserializePreparedSolanaTransaction(serialized) {
103
+ if (serialized instanceof Transaction || serialized instanceof VersionedTransaction) {
104
+ return serialized;
105
+ }
106
+ const bytes = toSerializedTransactionBytes(serialized);
107
+ try {
108
+ return VersionedTransaction.deserialize(bytes);
109
+ }
110
+ catch {
111
+ // fall back to legacy Transaction
112
+ }
113
+ try {
114
+ return Transaction.from(bytes);
115
+ }
116
+ catch {
117
+ throw new Error(INVALID_TX_PAYLOAD_ERROR);
118
+ }
119
+ }
59
120
  async function waitForTransactionConfirmation(signature, connection, lastValidBlockHeight) {
60
121
  const maxRetries = 30;
61
122
  const retryDelay = 2000;
@@ -1,3 +1,4 @@
1
+ import { getSuiPaymentData } from "@coin-voyage/shared/common";
1
2
  import { useReportTransactionEffects, useSignTransaction, useSuiClient } from "@mysten/dapp-kit";
2
3
  import { Transaction } from "@mysten/sui/transactions";
3
4
  import { SUI_PACKAGE_IDS } from "./constants";
@@ -6,25 +7,50 @@ export function useSUITransaction() {
6
7
  const { mutateAsync: signTransaction } = useSignTransaction();
7
8
  const { mutate: reportTransactionEffects } = useReportTransactionEffects();
8
9
  const execute = async (params) => {
9
- const { fromAddress, toAddress, amount, token } = params;
10
+ const preparedPayment = getSuiPaymentData(params.paymentData);
11
+ if (preparedPayment) {
12
+ const transaction = typeof preparedPayment.transaction === "string"
13
+ ? preparedPayment.transaction
14
+ : Transaction.from(preparedPayment.transaction);
15
+ const { bytes, signature } = await signTransaction({
16
+ transaction,
17
+ });
18
+ const executeResult = await client.executeTransactionBlock({
19
+ transactionBlock: bytes,
20
+ signature,
21
+ options: {
22
+ showRawEffects: true,
23
+ },
24
+ });
25
+ if (executeResult.rawEffects) {
26
+ reportTransactionEffects({
27
+ effects: executeResult.rawEffects,
28
+ });
29
+ }
30
+ return executeResult.digest;
31
+ }
32
+ const { from: owner, to, amount, token } = params;
33
+ if (!to || amount == undefined) {
34
+ throw new Error("Missing Sui transfer target or amount");
35
+ }
10
36
  const tokenAddress = token?.address;
11
37
  const isNative = !tokenAddress || tokenAddress.toLowerCase() === SUI_PACKAGE_IDS["sui"].toLowerCase();
12
38
  const tx = new Transaction();
13
39
  const value = BigInt(amount);
14
40
  if (isNative) {
15
41
  const balance = await client.getBalance({
16
- owner: fromAddress,
42
+ owner,
17
43
  coinType: SUI_PACKAGE_IDS["sui"],
18
44
  });
19
45
  if (BigInt(balance.totalBalance) < value) {
20
46
  throw new Error(`[CHECKOUT] Insufficient balance: ${balance}`);
21
47
  }
22
48
  const [coin] = tx.splitCoins(tx.gas, [value]);
23
- tx.transferObjects([coin], toAddress);
49
+ tx.transferObjects([coin], to);
24
50
  }
25
51
  else {
26
52
  const coins = await client.getCoins({
27
- owner: fromAddress,
53
+ owner,
28
54
  coinType: tokenAddress,
29
55
  });
30
56
  let total = BigInt(0);
@@ -42,7 +68,7 @@ export function useSUITransaction() {
42
68
  tx.mergeCoins(coinInput, coinInputs.slice(1));
43
69
  }
44
70
  const [paymentCoin] = tx.splitCoins(coinInput, [value]);
45
- tx.transferObjects([paymentCoin], toAddress);
71
+ tx.transferObjects([paymentCoin], to);
46
72
  }
47
73
  const gasPrice = await client.getReferenceGasPrice();
48
74
  tx.setGasPrice(gasPrice);
@@ -1,10 +1,12 @@
1
+ import type { CryptoPaymentData } from "@coin-voyage/shared/types";
1
2
  export type ExecuteTransaction = {
2
- fromAddress: string;
3
- toAddress: string;
4
- amount: bigint;
3
+ from: string;
4
+ to?: string;
5
+ amount?: bigint;
5
6
  chainId?: number;
6
7
  token?: {
7
8
  address: string;
8
9
  decimals: number;
9
10
  };
11
+ paymentData?: CryptoPaymentData;
10
12
  };
@@ -1,5 +1,5 @@
1
1
  import { type Config, CreateConnectorFn } from "@bigmi/client";
2
- import { HttpTransportConfig } from "@bigmi/core";
2
+ import { type HttpTransportConfig } from "@bigmi/core";
3
3
  /**
4
4
  * UTXO Configuration for Wallets and Connectors
5
5
  * Includes options for Bigmi-specific configurations
@@ -1,6 +1,6 @@
1
1
  import { createConfig, leather, magicEden, okx, onekey, phantom, unisat, xverse, } from "@bigmi/client";
2
- import { bitcoin, createClient, http } from "@bigmi/core";
3
- import { ALCHEMY_KEY } from "../evm/utils";
2
+ import { bitcoin, createClient } from "@bigmi/core";
3
+ import { createUTXOTransport } from "./create-utxo-transport";
4
4
  export function createDefaultBigmiConfig(props = {
5
5
  additionalConfigOptions: { multiInjectedProviderDiscovery: false },
6
6
  }) {
@@ -19,11 +19,10 @@ export function createDefaultBigmiConfig(props = {
19
19
  connectors,
20
20
  client({ chain }) {
21
21
  const chainRpcConfig = props?.rpcConfig;
22
- let config = [http(), http(`https://bitcoin-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}`)];
23
- if (chainRpcConfig) {
24
- config = [http(chainRpcConfig.url, chainRpcConfig.config), http()];
25
- }
26
- return createClient({ chain, transport: config[0] });
22
+ return createClient({
23
+ chain,
24
+ transport: createUTXOTransport(chainRpcConfig),
25
+ });
27
26
  },
28
27
  ...props?.additionalConfigOptions,
29
28
  });
@@ -40,7 +40,7 @@ export async function createPsbtTx(client, fromAddress, toAddress, amountSats) {
40
40
  txid: utxo.txId,
41
41
  index: utxo.vout,
42
42
  sequence: 0xfffffffd, // RBF
43
- ...(addressType === AddressType.p2wpkh || addressType === AddressType.p2wsh
43
+ ...(addressType === AddressType.p2wpkh || addressType === AddressType.p2wsh || addressType === AddressType.p2tr
44
44
  ? {
45
45
  witnessUtxo: {
46
46
  script: scripts.script,
@@ -0,0 +1,6 @@
1
+ import { type HttpTransportConfig } from "@bigmi/core";
2
+ export interface UTXORpcConfig {
3
+ url?: string | undefined;
4
+ config?: HttpTransportConfig;
5
+ }
6
+ export declare function createUTXOTransport(rpcConfig?: UTXORpcConfig): import("@bigmi/core").FallbackTransport<readonly [import("@bigmi/core").HttpTransport<undefined, false>, import("@bigmi/core").HttpTransport<import("@bigmi/core").RpcSchema, false>, import("@bigmi/core").HttpTransport<undefined, false>, import("@bigmi/core").HttpTransport<undefined, false>, import("@bigmi/core").HttpTransport<undefined, false>, import("@bigmi/core").HttpTransport<undefined, false>]>;
@@ -0,0 +1,17 @@
1
+ import { ankr, blockchair, blockcypher, fallback, http, mempool } from "@bigmi/core";
2
+ import { ALCHEMY_KEY } from "../evm/utils";
3
+ export function createUTXOTransport(rpcConfig) {
4
+ const primaryRpc = rpcConfig
5
+ ? http(rpcConfig.url, rpcConfig.config)
6
+ : http(`https://bitcoin-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}`);
7
+ return fallback([
8
+ primaryRpc,
9
+ http(),
10
+ blockchair(),
11
+ blockcypher(),
12
+ mempool(),
13
+ ankr({
14
+ apiKey: "2a2801137db177be9c80522e690ec8b59ffc5260d030b2671b5e3936b917daac",
15
+ }),
16
+ ]);
17
+ }
@@ -1,4 +1,17 @@
1
+ import { Account } from "@bigmi/core";
1
2
  import { UTXOConnectorId } from "../types/utxo-connector-id";
3
+ type PhantomBitcoinProvider = {
4
+ isPhantom?: boolean;
5
+ requestAccounts(): Promise<Account[]>;
6
+ signPSBT(psbt: Uint8Array, options: {
7
+ inputsToSign: {
8
+ address: string;
9
+ signingIndexes: number[];
10
+ sigHash?: number;
11
+ }[];
12
+ finalize?: boolean;
13
+ }): Promise<Uint8Array>;
14
+ };
2
15
  declare global {
3
16
  interface Window {
4
17
  LeatherProvider?: any;
@@ -8,7 +21,7 @@ declare global {
8
21
  bitcoin?: any;
9
22
  };
10
23
  phantom?: {
11
- bitcoin?: any;
24
+ bitcoin?: PhantomBitcoinProvider;
12
25
  };
13
26
  xfi?: {
14
27
  bitcoin?: any;
@@ -1,5 +1,6 @@
1
1
  import { sendUTXOTransaction } from "@bigmi/core";
2
2
  import { ChainId } from "@coin-voyage/shared/types";
3
+ import { hex } from "@scure/base";
3
4
  import { Psbt } from "bitcoinjs-lib";
4
5
  import { createUnsecuredToken } from "jsontokens";
5
6
  import { withTimeout } from "viem";
@@ -139,30 +140,22 @@ async function sendBtcPhantom(recipient) {
139
140
  }
140
141
  const publicClient = await getUTXOPublicClient(ChainId.BTC);
141
142
  const tx = await createPsbtTx(publicClient, paymentAccount, recipient.address, Number(recipient.amount));
142
- const payAddress = paymentAccount.address;
143
- const inputsToSign = new Map();
144
- for (let i = 0; i < tx.inputsLength; i++) {
145
- const input = tx.getInput(i);
146
- if (inputsToSign.has(payAddress)) {
147
- inputsToSign.get(payAddress).signingIndexes.push(input.index);
148
- }
149
- else {
150
- inputsToSign.set(payAddress, {
151
- address: payAddress,
152
- sigHash: 0,
153
- signingIndexes: [input.index],
154
- });
155
- }
156
- }
143
+ const psbtBytes = tx.toPSBT();
144
+ const inputsToSign = [
145
+ {
146
+ address: paymentAccount.address,
147
+ signingIndexes: Array.from({ length: tx.inputsLength }, (_, index) => index),
148
+ },
149
+ ];
157
150
  try {
158
151
  // We give users 10 minutes to sign the transaction or it should be considered expired
159
- const signedPsbtHex = await withTimeout(() => provider.signPSBT(tx.unsignedTx, {
160
- inputsToSign: Array.from(inputsToSign.values()),
152
+ const signedPsbtBytes = await withTimeout(() => provider.signPSBT(psbtBytes, {
153
+ inputsToSign,
161
154
  }), {
162
155
  timeout: 600000,
163
156
  errorInstance: new Error("Transaction has expired."),
164
157
  });
165
- const signedPsbt = Psbt.fromHex(signedPsbtHex).finalizeAllInputs();
158
+ const signedPsbt = Psbt.fromHex(hex.encode(signedPsbtBytes)).finalizeAllInputs();
166
159
  const txHex = signedPsbt.extractTransaction().toHex();
167
160
  // const signedTx = btc.Transaction.fromRaw(signedPSBTBytes)
168
161
  // signedTx.finalize()
@@ -1,12 +1,20 @@
1
1
  import { useAccount } from "@bigmi/react";
2
+ import { getBitcoinPaymentData } from "@coin-voyage/shared/common";
2
3
  import { sendBtc } from "./send-btc";
3
4
  export function useUTXOTransaction() {
4
5
  const { account, connector } = useAccount();
5
6
  const execute = async (params) => {
6
- const { toAddress, amount } = params;
7
+ const bitcoinPaymentData = getBitcoinPaymentData(params.paymentData);
8
+ if (bitcoinPaymentData) {
9
+ throw new Error("Prepared Bitcoin transactions are not supported in wallet flow yet.");
10
+ }
11
+ const { to: toAddress, amount } = params;
7
12
  if (!connector?.id || !account?.address) {
8
13
  throw new Error("No connection found");
9
14
  }
15
+ if (!toAddress || amount == undefined) {
16
+ throw new Error("Missing Bitcoin transfer target or amount");
17
+ }
10
18
  const txhash = await sendBtc({
11
19
  senderAddress: account.address,
12
20
  recipient: toAddress,
@@ -1,7 +1,10 @@
1
- import { type UTXOSchema, Chain, Client, FallbackTransport, HttpTransport, PublicActions, WalletActions } from "@bigmi/core";
1
+ import { type Client, type PublicActions, type UTXOSchema, type WalletActions, bitcoin } from "@bigmi/core";
2
+ import { createUTXOTransport } from "./create-utxo-transport";
3
+ type UTXOPublicClient = Client<ReturnType<typeof createUTXOTransport>, typeof bitcoin, undefined, UTXOSchema, PublicActions & WalletActions>;
2
4
  /**
3
5
  * Get an instance of a provider for a specific chain
4
6
  * @param chainId - Id of the chain the provider is for
5
7
  * @returns The public client for the given chain
6
8
  */
7
- export declare const getUTXOPublicClient: (chainId: number) => Promise<Client<FallbackTransport<readonly HttpTransport[]>, Chain, undefined, UTXOSchema, PublicActions & WalletActions>>;
9
+ export declare const getUTXOPublicClient: (chainId: number) => Promise<UTXOPublicClient>;
10
+ export {};
@@ -1,5 +1,15 @@
1
- import { ankr, bitcoin, blockchair, blockcypher, createClient, fallback, mempool, publicActions, rpcSchema, walletActions, } from "@bigmi/core";
2
- // cached providers
1
+ import { bitcoin, createClient, publicActions, rpcSchema, walletActions, } from "@bigmi/core";
2
+ import { createUTXOTransport } from "./create-utxo-transport";
3
+ function createUTXOPublicClient() {
4
+ return createClient({
5
+ chain: bitcoin,
6
+ rpcSchema: rpcSchema(),
7
+ transport: createUTXOTransport(),
8
+ pollingInterval: 10000,
9
+ })
10
+ .extend(publicActions)
11
+ .extend(walletActions);
12
+ }
3
13
  const publicClients = {};
4
14
  /**
5
15
  * Get an instance of a provider for a specific chain
@@ -8,15 +18,7 @@ const publicClients = {};
8
18
  */
9
19
  export const getUTXOPublicClient = async (chainId) => {
10
20
  if (!publicClients[chainId]) {
11
- const client = createClient({
12
- chain: bitcoin,
13
- rpcSchema: rpcSchema(),
14
- transport: fallback([blockchair(), blockcypher(), mempool(), ankr()]),
15
- pollingInterval: 10000,
16
- })
17
- .extend(publicActions)
18
- .extend(walletActions);
19
- publicClients[chainId] = client;
21
+ publicClients[chainId] = createUTXOPublicClient();
20
22
  }
21
23
  if (!publicClients[chainId]) {
22
24
  throw new Error(`Unable to configure provider for chain ${chainId}`);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@coin-voyage/crypto",
3
3
  "description": "Crypto utilities for Coin Voyage",
4
- "version": "2.2.3",
4
+ "version": "2.3.1-beta.0",
5
5
  "private": false,
6
6
  "sideEffects": false,
7
7
  "author": "Lars <lars@coinvoyage.io>",
@@ -50,7 +50,7 @@
50
50
  "@solana/wallet-adapter-walletconnect": "0.1.21",
51
51
  "@solana/wallet-adapter-base": "0.9.27",
52
52
  "@solana/wallet-adapter-coinbase": "0.1.23",
53
- "@coin-voyage/shared": "2.2.5"
53
+ "@coin-voyage/shared": "2.3.5-beta.3"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@types/elliptic": "6.4.18"