Send tokens

This commit is contained in:
2025-07-06 14:39:01 +07:00
parent c7dec76809
commit f973be2e08
13 changed files with 693 additions and 12 deletions

View File

@@ -44,6 +44,7 @@ declare module 'fastify' {
signPrivyMessage: typeof signPrivyMessage;
approveToken: typeof approveToken;
initAddress: typeof initAddress;
sendToken: typeof sendToken;
}
}
@@ -186,6 +187,15 @@ const tokenApprovalSchema = z.object({
chainId: z.number().positive().optional()
});
// Schema for token-sending request
const tokenSendSchema = z.object({
senderAddress: z.string().nonempty(),
recipientAddress: z.string().nonempty(),
ticker: z.string().nonempty(),
amount: z.bigint().positive(),
chainId: z.number().positive().optional()
});
/**
* Gets the chain name based on chain ID
* @param chainId The chain ID
@@ -642,6 +652,141 @@ export async function initAddress(
}
}
/**
* Sends tokens from one address to another using Privy wallet (implementation)
* @param senderAddress The sender's wallet address
* @param recipientAddress The recipient's wallet address
* @param ticker The token ticker or enum value
* @param amount The amount to send
* @param chainId The chain ID (optional, defaults to ARBITRUM)
* @returns The transaction hash
*/
export const sendTokenImpl = async (
senderAddress: string,
recipientAddress: string,
ticker: string,
amount: bigint,
chainId?: number,
): Promise<string> => {
try {
// Get token data from ticker
const tokenData = GetToken(ticker);
// Check if sender has sufficient allowance for the token transfer
const senderAllowance = await getTokenAllowance(senderAddress, tokenData.address, senderAddress);
// If insufficient allowance, approve the token first
if (senderAllowance < amount) {
console.log(`Insufficient allowance (${senderAllowance}). Approving token for amount: ${amount}`);
await approveContractImpl(
senderAddress,
tokenData.address,
senderAddress, // Approve self to spend tokens
chainId ?? ARBITRUM,
amount
);
console.log('Token approval completed');
}
// Create contract interface for ERC20 token
const contractInterface = new ethers.Interface(Token.abi);
// Convert amount to the correct decimal format
const transferAmount = ethers.parseUnits(amount.toString(), tokenData.decimals);
// Encode the transfer function call
const data = contractInterface.encodeFunctionData("transfer", [recipientAddress, transferAmount]);
chainId = chainId ?? ARBITRUM;
// Get chain name in CAIP-2 format
const networkName = getChainName(chainId);
const privy = getPrivyClient();
// Send the transaction
const { hash } = await privy.walletApi.ethereum.sendTransaction({
address: senderAddress as Address,
chainType: 'ethereum',
caip2: networkName as string,
transaction: {
to: tokenData.address as Address,
data: data,
chainId: chainId,
},
} as any);
return hash;
} catch (error) {
console.error('Error sending token:', error);
throw new Error(`Failed to send token: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
};
/**
* Sends tokens from one address to another using Privy wallet
* @param this The FastifyRequest instance
* @param reply The FastifyReply instance
* @param senderAddress The sender's wallet address
* @param recipientAddress The recipient's wallet address
* @param ticker The token ticker or enum value
* @param amount The amount to send
* @param chainId The chain ID (optional, defaults to ARBITRUM)
* @returns The response object with success status and transaction hash
*/
export async function sendToken(
this: FastifyRequest,
reply: FastifyReply,
senderAddress: string,
recipientAddress: string,
ticker: string,
amount: bigint,
chainId?: number
) {
try {
// Validate the request parameters
tokenSendSchema.parse({
senderAddress,
recipientAddress,
ticker,
amount,
chainId
});
if (!senderAddress) {
throw new Error('Sender address is required for token transfer');
}
if (!recipientAddress) {
throw new Error('Recipient address is required for token transfer');
}
if (!ticker) {
throw new Error('Token ticker is required for token transfer');
}
if (!amount || amount <= 0n) {
throw new Error('Valid amount is required for token transfer');
}
// Call the sendTokenImpl function
const hash = await sendTokenImpl(senderAddress, recipientAddress, ticker, amount, chainId);
return {
success: true,
hash: hash
};
} catch (error) {
this.log.error(error);
// Return appropriate error response
reply.status(error instanceof z.ZodError ? 400 : 500);
return {
success: false,
error: error instanceof Error ? error.message : 'An unknown error occurred'
};
}
}
/**
* The use of fastify-plugin is required to be able
* to export the decorators to the outer scope
@@ -662,6 +807,10 @@ export default fp(async (fastify) => {
return initAddress.call(this, reply, address);
});
fastify.decorateRequest('sendToken', async function(this: FastifyRequest, reply: FastifyReply, senderAddress: string, recipientAddress: string, ticker: string, amount: bigint, chainId?: number) {
return sendToken.call(this, reply, senderAddress, recipientAddress, ticker, amount, chainId);
});
// Test the Privy client initialization
try {
const testClient = getPrivyClient(fastify);