From 339d50e96f2b0cf7140b97d6ce2fe12bd84ff7ee Mon Sep 17 00:00:00 2001 From: xixixueling Date: Mon, 16 Dec 2024 16:04:15 +0800 Subject: [PATCH] add benfen&ton&sui&sol token transfer (#266) --- .../components/chains/benfen/example.tsx | 109 +++++++++++++++++- .../components/chains/benfen/params.ts | 12 ++ .../components/chains/solana/builder.ts | 39 +++++++ .../components/chains/solana/example.tsx | 69 +++++++++-- .../components/chains/solana/params.ts | 28 +++++ .../components/chains/suiStandard/example.tsx | 105 ++++++++++++++++- .../components/chains/suiStandard/params.ts | 12 ++ .../example/components/chains/ton/example.tsx | 41 ++++++- .../example/components/chains/ton/params.ts | 94 +++++++++++++++ 9 files changed, 488 insertions(+), 21 deletions(-) diff --git a/packages/example/components/chains/benfen/example.tsx b/packages/example/components/chains/benfen/example.tsx index f139cf72..e038db8c 100644 --- a/packages/example/components/chains/benfen/example.tsx +++ b/packages/example/components/chains/benfen/example.tsx @@ -41,6 +41,7 @@ import { WalletWithRequiredFeatures } from '@benfen/bfc.js/dist/cjs/wallet-stand function Example() { const client = useBenfenClient(); const { setProvider } = useWallet(); + const [customToAddress, setCustomToAddress] = useState(''); const currentAccount = useCurrentAccount(); const { isConnected } = useCurrentWallet(); @@ -64,10 +65,14 @@ function Example() { return params.signTransaction(currentAccount?.address ?? ''); }, [currentAccount?.address]); + const signTokenTransactionParams = useMemo(() => { + return params.signTokenTransaction(currentAccount?.address ?? ''); + }, [currentAccount?.address]); + return ( <> - + /> */} { const res = await signPersonalMessage({ @@ -196,7 +201,7 @@ function Example() { const transfer = new TransactionBlock(); transfer.setSender(from); const [coin] = transfer.splitCoins(transfer.gas, [transfer.pure(amount)]); - transfer.transferObjects([coin], transfer.pure(to)); + transfer.transferObjects([coin], transfer.pure(customToAddress || to)); const tx = await sponsorTransaction( client, @@ -276,6 +281,102 @@ function Example() { ).toString(); }} /> + + { + const { from, to, amount ,token} = JSON.parse(request); + + const transfer = new TransactionBlock(); + transfer.setSender(from); + + const { data: coins } = await client.getCoins({ + owner: from, + coinType: token, + }); + + if (!coins.length) { + throw new Error('No BUSD coins found'); + } + + const [coin] = transfer.splitCoins( + transfer.object(coins[0].coinObjectId), + [transfer.pure(amount)] + ); + transfer.transferObjects([coin], transfer.pure(to)); + + const tx = await sponsorTransaction( + client, + from, + await transfer.build({ + client, + onlyTransactionKind: true, + }), + ); + + const res = await signTransactionBlock({ + transactionBlock: tx, + account: currentAccount, + }); + return JSON.stringify(res); + }} + onValidate={async (request: string, result: string) => { + const { transactionBlockBytes, signature } = JSON.parse(result); + const publicKey = await verifyTransactionBlock( + Buffer.from(transactionBlockBytes, 'base64'), + signature, + ); + + return ( + bytesToHex(currentAccount.publicKey) === bytesToHex(publicKey.toRawBytes()) + ).toString(); + }} + /> + + { + const { from, to, amount, token } = JSON.parse(request); + + const transfer = new TransactionBlock(); + transfer.setSender(from); + + const { data: coins } = await client.getCoins({ + owner: from, + coinType: token, + }); + + if (!coins.length) { + throw new Error('No BUSD coins found'); + } + + const [coin] = transfer.splitCoins( + transfer.object(coins[0].coinObjectId), + [transfer.pure(BigInt(amount))] + ); + transfer.transferObjects([coin], transfer.pure(to)); + + const tx = await sponsorTransaction( + client, + from, + await transfer.build({ + client, + onlyTransactionKind: true, + }), + ); + + const res = await signAndExecuteTransactionBlock({ + transactionBlock: tx, + account: currentAccount, + }); + + return JSON.stringify(res); + }} + /> diff --git a/packages/example/components/chains/benfen/params.ts b/packages/example/components/chains/benfen/params.ts index ce6508d5..18a3994b 100644 --- a/packages/example/components/chains/benfen/params.ts +++ b/packages/example/components/chains/benfen/params.ts @@ -34,4 +34,16 @@ export default { }), }, ], + signTokenTransaction: (address: string) => [ + { + id: 'signUSDTransaction', + name: 'BUSD_TYPE', + value: JSON.stringify({ + from: address, + to: address, + amount: 1000, // 0.000001 USD + token: '0x00000000000000000000000000000000000000000000000000000000000000c8::busd::BUSD' + }), + }, + ], }; diff --git a/packages/example/components/chains/solana/builder.ts b/packages/example/components/chains/solana/builder.ts index 7b9928ea..d4609b1e 100644 --- a/packages/example/components/chains/solana/builder.ts +++ b/packages/example/components/chains/solana/builder.ts @@ -6,7 +6,9 @@ import { TransactionMessage, VersionedMessage, VersionedTransaction, + Connection, } from '@solana/web3.js'; +import { TOKEN_PROGRAM_ID, createTransferInstruction, getAssociatedTokenAddress } from '@solana/spl-token'; export const createTransferTransaction = ( publicKey: PublicKey, @@ -54,3 +56,40 @@ export const createVersionedTransaction = ( const transaction = new VersionedTransaction(messageV0); return transaction; }; + +// 将 async function 改为箭头函数形式的导出 +export const createTokenTransferTransaction = async ( + connection: Connection, + fromPubkey: PublicKey, + toPubkey: PublicKey, + tokenMint: PublicKey, + recentBlockhash: string, + amount: number, + decimals: number +): Promise => { + const transaction = new Transaction(); + + const fromTokenAccount = await getAssociatedTokenAddress( + tokenMint, + fromPubkey + ); + + const toTokenAccount = await getAssociatedTokenAddress( + tokenMint, + toPubkey + ); + + transaction.add( + createTransferInstruction( + fromTokenAccount, + toTokenAccount, + fromPubkey, + BigInt(amount * Math.pow(10, decimals)), + ) + ); + + transaction.feePayer = fromPubkey; + transaction.recentBlockhash = recentBlockhash; + + return transaction; +}; diff --git a/packages/example/components/chains/solana/example.tsx b/packages/example/components/chains/solana/example.tsx index 127baade..7ff97d3a 100644 --- a/packages/example/components/chains/solana/example.tsx +++ b/packages/example/components/chains/solana/example.tsx @@ -1,18 +1,19 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-return */ import { dapps } from './dapps.config'; -import ConnectButton from '../../../components/connect/ConnectButton'; +import ConnectButton from '../../connect/ConnectButton'; import { useEffect, useMemo, useRef } from 'react'; import { get, set } from 'lodash-es'; import { IProviderApi, IProviderInfo } from './types'; import { ApiPayload, ApiGroup } from '../../ApiActuator'; -import { useWallet } from '../../../components/connect/WalletContext'; -import type { IKnownWallet } from '../../../components/connect/types'; -import DappList from '../../../components/DAppList'; +import { useWallet } from '../../connect/WalletContext'; +import type { IKnownWallet } from '../../connect/types'; +import DappList from '../../DAppList'; import { Connection, PublicKey, Transaction, VersionedTransaction, clusterApiUrl } from '@solana/web3.js'; import params from './params'; -import { createTransferTransaction, createVersionedTransaction } from './builder'; +import { createTransferTransaction, createVersionedTransaction, createTokenTransferTransaction } from './builder'; import nacl from 'tweetnacl'; import { toast } from '../../ui/use-toast'; +// import { TOKEN_PROGRAM_ID } from '@solana/spl-token'; const NETWORK = clusterApiUrl('mainnet-beta'); @@ -103,6 +104,21 @@ export default function Example() { }; }, [account, provider, setAccount]); + console.log('createTokenTransferTransaction exists:', typeof createTokenTransferTransaction); + + const transactionStatus = { + status: 'pending', + message: ` + 交易尚未在本地确认 + + 这是正常的,因为交易刚刚被广播到网络 + + 交易需要等待网络确认,这个过程需要一些时间 + + 请等待区块确认... + ` + }; + return ( <> @@ -216,16 +232,19 @@ export default function Example() { return Buffer.from(res.serialize()).toString('hex') }} onValidate={async (request: string, result: string) => { - const tx = Transaction.from(Buffer.from(result, 'hex')) - const verified = tx.verifySignatures() + const tx = Transaction.from(Buffer.from(result, 'hex')); + const verified = tx.verifySignatures(); + + const res = await connection.simulateTransaction(tx, { + sigVerify: false, + }); - const res = await connection.simulateTransaction(tx) return { success: res.value.err === null, verified, tryRun: res, tx - } + }; }} /> + { + const { + tokenMint, + toPubkey, + amount, + decimals + }: { + tokenMint: string; + toPubkey: string; + amount: number; + decimals: number; + } = JSON.parse(request); + + const recentBlockhash = (await connection.getLatestBlockhash()).blockhash; + const transaction = await createTokenTransferTransaction( + connection, + new PublicKey(account?.publicKey), + new PublicKey(toPubkey), + new PublicKey(tokenMint), + recentBlockhash, + amount, + decimals + ); + + const res = await provider?.signAndSendTransaction(transaction); + return JSON.stringify(res); + }} + /> diff --git a/packages/example/components/chains/solana/params.ts b/packages/example/components/chains/solana/params.ts index 70d18cee..3ca92a0b 100644 --- a/packages/example/components/chains/solana/params.ts +++ b/packages/example/components/chains/solana/params.ts @@ -1,3 +1,20 @@ +const TOKEN_LIST = [ + { + symbol: 'USDT', + tokenMint: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', + decimals: 6 + }, + { + symbol: 'USDC', + tokenMint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', + decimals: 6 + }, + // { + // symbol: 'RNDR', + // tokenMint: '7atgF8KQo4wJrD5ATGX7t1V2zVvykPJbFfNeVf1icFv1', + // decimals: 8 + // }, +]; export default { signMessage: [ { @@ -16,6 +33,17 @@ export default { }), }, ], + signAndSendTokenTransaction: (publicKey: string) => + TOKEN_LIST.map(token => ({ + id: `signAndSendTokenTransaction_${token.symbol}`, + name: `Send ${token.symbol} Token`, + value: JSON.stringify({ + tokenMint: token.tokenMint, + toPubkey: publicKey, + amount: 0.000001, + decimals: token.decimals + }), + })), signMultipleTransaction: (publicKey: string) => [ { id: 'signMultipleTransaction', diff --git a/packages/example/components/chains/suiStandard/example.tsx b/packages/example/components/chains/suiStandard/example.tsx index ded3c5b1..baf65017 100644 --- a/packages/example/components/chains/suiStandard/example.tsx +++ b/packages/example/components/chains/suiStandard/example.tsx @@ -4,7 +4,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { dapps } from './dapps.config'; import { useContext, useEffect, useMemo, useRef, useState } from 'react'; -import { hexToBytes } from '@noble/hashes/utils'; +import { hexToBytes,bytesToHex } from '@noble/hashes/utils'; import { useWallet } from '../../../components/connect/WalletContext'; import DappList from '../../../components/DAppList'; import params from './params'; @@ -219,6 +219,10 @@ function Example() { return params.signTransaction(currentAccount?.address ?? ''); }, [currentAccount?.address]); + const signTokenTransactionParams = useMemo(() => { + return params.signTokenTransaction(currentAccount?.address ?? ''); + }, [currentAccount?.address]); + return ( <> @@ -239,7 +243,7 @@ function Example() { - + /> */} + { + const { from, to, amount ,token} = JSON.parse(request); + + const transfer = new TransactionBlock(); + transfer.setSender(from); + + const { data: coins } = await client.getCoins({ + owner: from, + coinType: token, + }); + + if (!coins.length) { + throw new Error('No BUSD coins found'); + } + + const [coin] = transfer.splitCoins( + transfer.object(coins[0].coinObjectId), + [transfer.pure(amount)] + ); + transfer.transferObjects([coin], transfer.pure(to)); + + const tx = await sponsorTransaction( + client, + from, + await transfer.build({ + client, + onlyTransactionKind: true, + }), + ); + + const res = await signTransactionBlock({ + transactionBlock: tx, + account: currentAccount, + }); + return JSON.stringify(res); + }} + onValidate={async (request: string, result: string) => { + const { transactionBlockBytes, signature } = JSON.parse(result); + const publicKey = await verifyTransactionBlock( + Buffer.from(transactionBlockBytes, 'base64'), + signature, + ); + + return ( + bytesToHex(currentAccount.publicKey) === bytesToHex(publicKey.toRawBytes()) + ).toString(); + }} + /> + + { + const { from, to, amount, token } = JSON.parse(request); + + const transfer = new TransactionBlock(); + transfer.setSender(from); + + const { data: coins } = await client.getCoins({ + owner: from, + coinType: token, + }); + + if (!coins.length) { + throw new Error('No BUSD coins found'); + } + + const [coin] = transfer.splitCoins( + transfer.object(coins[0].coinObjectId), + [transfer.pure(BigInt(amount))] + ); + transfer.transferObjects([coin], transfer.pure(to)); + + const tx = await sponsorTransaction( + client, + from, + await transfer.build({ + client, + onlyTransactionKind: true, + }), + ); + + const res = await signAndExecuteTransactionBlock({ + transactionBlock: tx, + account: currentAccount, + }); + + return JSON.stringify(res); + }} + /> diff --git a/packages/example/components/chains/suiStandard/params.ts b/packages/example/components/chains/suiStandard/params.ts index ce6508d5..02e3fee8 100644 --- a/packages/example/components/chains/suiStandard/params.ts +++ b/packages/example/components/chains/suiStandard/params.ts @@ -34,4 +34,16 @@ export default { }), }, ], + signTokenTransaction: (address: string) => [ + { + id: 'signUSDCransaction', + name: 'USDC_TYPE', + value: JSON.stringify({ + from: address, + to: address, + amount: 1000, // 0.000001 USD + token: '0x5d4b302506645c37ff133b98c4b50a5ae14841659738d6d733d59d0d217a93bf::coin::COIN' + }), + }, + ], }; diff --git a/packages/example/components/chains/ton/example.tsx b/packages/example/components/chains/ton/example.tsx index 70c94cc7..4daee2ff 100644 --- a/packages/example/components/chains/ton/example.tsx +++ b/packages/example/components/chains/ton/example.tsx @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-return */ import { dapps } from './dapps.config'; import { ApiPayload, ApiGroup } from '../../ApiActuator'; -import DappList from '../../../components/DAppList'; +import DappList from '../../DAppList'; import { TonConnectButton, TonConnectUIProvider, @@ -14,15 +14,33 @@ import params from './params'; import { TonProofDemoApi } from './TonProofDemoApi'; import { Switch } from '../../ui/switch'; import { useToast } from '../../ui/use-toast'; - +import { useState, useEffect } from 'react'; const TON_SCAM_DAPP_ENABLE_KEY = 'ton_scam_dapp_enable'; +type IPresupposeParam = { + id: string; + name: string; + value: string; + description?: string; +}; + export function Example() { const userFriendlyAddress = useTonAddress(); const rawAddress = useTonAddress(false); const wallet = useTonWallet(); const [tonConnectUI, setOptions] = useTonConnectUI(); const { toast } = useToast(); + const [tokenParams, setTokenParams] = useState([]); + + useEffect(() => { + async function fetchTokenParams() { + if (userFriendlyAddress) { + const result = await params.sendTokenTransaction(userFriendlyAddress); + setTokenParams(result); + } + } + void fetchTokenParams(); + }, [userFriendlyAddress]); const scamEnable = localStorage.getItem(TON_SCAM_DAPP_ENABLE_KEY); @@ -60,7 +78,7 @@ export function Example() { )} - + + + { + const res = await tonConnectUI?.sendTransaction(JSON.parse(request)); + return JSON.stringify(res); + }} + /> + + ); diff --git a/packages/example/components/chains/ton/params.ts b/packages/example/components/chains/ton/params.ts index 55a6f6c3..b1903985 100644 --- a/packages/example/components/chains/ton/params.ts +++ b/packages/example/components/chains/ton/params.ts @@ -1,3 +1,29 @@ +const { Address, beginCell } = require("@ton/core"); +const { TonClient, JettonMaster } = require("@ton/ton"); + +const client = new TonClient({ + endpoint: 'https://toncenter.com/api/v2/jsonRPC', +}); +async function getJettonWalletAddress(jettonMasterAddressStr: string, userAddressStr: string) { + try { + const jettonMasterAddress = Address.parse(jettonMasterAddressStr); + const userAddress = Address.parse(userAddressStr); + + const jettonMaster = client.open(JettonMaster.create(jettonMasterAddress)); + const walletAddress = await jettonMaster.getWalletAddress(userAddress); + + if (!walletAddress) { + throw new Error('Wallet address is empty'); + } + + console.log('Wallet Address:', walletAddress.toString()); + return walletAddress.toString(); + } catch (error) { + console.error('Error in getJettonWalletAddress:', error); + return null; // 或者根据需要返回其他值 + } +} + export default { sendTransaction: (to: string) => [ { @@ -88,4 +114,72 @@ export default { }, ]; }, + sendTokenTransaction: async (address: string) => { + // 定义支持的代币主合约地址 + const jettonMasters = { + SCALE: "EQBlqsm144Dq6SjbPI4jjZvA1hqTIP3CvHovbIfW_t-SCALE", + USDT: "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs" + }; + + // 获取所有代币的钱包地址 + const scaleWallet = await getJettonWalletAddress(jettonMasters.SCALE, address); + const usdtWallet = await getJettonWalletAddress(jettonMasters.USDT, address); + + if (!scaleWallet || !usdtWallet) { + throw new Error('无法获取代币钱包地址'); + } + + return [ + { + id: 'sendToken-scale', + name: 'Send SCALE Token', + value: JSON.stringify({ + validUntil: Math.floor(Date.now() / 1000) + 360, + messages: [{ + address: scaleWallet, + amount: "150000000", + payload: beginCell() + .storeUint(0xf8a7ea5, 32) + .storeUint(0, 64) + .storeCoins(10000000000) + .storeAddress(Address.parse(address)) + .storeAddress(Address.parse(address)) + .storeCoins(0) + .storeBit(0) + .storeBit(0) + .storeRef(beginCell().endCell()) + .endCell() + .toBoc() + .toString('base64'), + stateInit: null + }] + }) + }, + { + id: 'sendToken-usdt', + name: 'Send USDT Token', + value: JSON.stringify({ + validUntil: Math.floor(Date.now() / 1000) + 360, + messages: [{ + address: usdtWallet, + amount: "150000000", + payload: beginCell() + .storeUint(0xf8a7ea5, 32) + .storeUint(0, 64) + .storeCoins(100000) + .storeAddress(Address.parse(address)) + .storeAddress(Address.parse(address)) + .storeCoins(0) + .storeBit(0) + .storeBit(0) + .storeRef(beginCell().endCell()) + .endCell() + .toBoc() + .toString('base64'), + stateInit: null + }] + }) + } + ]; + }, };