Send tokens
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {FastifyPluginAsyncTypebox, Type} from '@fastify/type-provider-typebox'
|
||||
import { handleError } from '../../../utils/errorHandler.js'
|
||||
import {handleError} from '../../../utils/errorHandler.js'
|
||||
|
||||
const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
|
||||
fastify.post(
|
||||
@@ -75,6 +75,47 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
fastify.post(
|
||||
'/send-token',
|
||||
{
|
||||
schema: {
|
||||
body: Type.Object({
|
||||
senderAddress: Type.String(),
|
||||
recipientAddress: Type.String(),
|
||||
ticker: Type.String(),
|
||||
amount: Type.String(), // Using string for bigint compatibility
|
||||
chainId: Type.Optional(Type.Number())
|
||||
}),
|
||||
response: {
|
||||
200: Type.Object({
|
||||
success: Type.Boolean(),
|
||||
hash: Type.Optional(Type.String()),
|
||||
error: Type.Optional(Type.String())
|
||||
}),
|
||||
400: Type.Object({
|
||||
success: Type.Boolean(),
|
||||
error: Type.String()
|
||||
}),
|
||||
500: Type.Object({
|
||||
success: Type.Boolean(),
|
||||
error: Type.String()
|
||||
})
|
||||
},
|
||||
tags: ['Privy']
|
||||
}
|
||||
},
|
||||
async function (request, reply) {
|
||||
try {
|
||||
const { senderAddress, recipientAddress, ticker, amount, chainId } = request.body;
|
||||
// Convert amount string to bigint
|
||||
const amountBigInt = BigInt(amount);
|
||||
return await request.sendToken(reply, senderAddress, recipientAddress, ticker, amountBigInt, chainId);
|
||||
} catch (error) {
|
||||
return handleError(request, reply, error, 'privy/send-token');
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export default plugin
|
||||
Reference in New Issue
Block a user