Skip to content

Commit

Permalink
feat: enhance transfer action and account pool service with improved …
Browse files Browse the repository at this point in the history
…user-based transfers

- Added support for transferring tokens using user IDs instead of direct addresses
- Implemented UUID validation for user ID checks
- Updated transfer action to use account pool service for token transfers
- Added new methods in account pool service for Flow, FT, and ERC20 token transfers
- Improved error handling and logging for transfer transactions
- Enhanced type safety in core package's script query response type
  • Loading branch information
btspoony committed Jan 31, 2025
1 parent 47689e1 commit 631f973
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 70 deletions.
2 changes: 1 addition & 1 deletion packages/common/src/actions/get-flow-price.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export class GetPriceAction extends BaseFlowInjectableAction<GetPriceContent> {

if (resp.ok) {
callback?.({
text: format(resp.data, targetToken),
text: format(resp.data as number, targetToken),
content: {
success: true,
token: content.token,
Expand Down
10 changes: 6 additions & 4 deletions packages/common/src/actions/get-user-account-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,13 @@ export class GetUserAccountInfoAction implements Action {
}

if (!acctInfo) {
resp.error =
resp.error ??
`Failed to query account info for ${userId} from ${mainAddr}, please ensure the account exists.`;
const errorMsg =
resp.errorMessage ??
(typeof resp.error === "string"
? (resp.error as string)
: `Failed to query account info for ${userId} from ${mainAddr}, please ensure the account exists.`);
callback?.({
text: resp.error,
text: errorMsg,
content: {
error: resp.error,
},
Expand Down
140 changes: 77 additions & 63 deletions packages/common/src/actions/transfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,10 @@ import {
type Memory,
type State,
} from "@elizaos/core";
import {
isCadenceIdentifier,
isEVMAddress,
queries as defaultQueries,
transactions,
} from "@elizaos/plugin-flow";
import { isCadenceIdentifier, isEVMAddress, isFlowAddress } from "@elizaos/plugin-flow";
import { type ActionOptions, globalContainer, property } from "@elizaos/plugin-di";
import { BaseFlowInjectableAction } from "@fixes-ai/core";

import { formatTransationSent } from "../formater";
import { AccountsPoolService } from "../services/acctPool.service";

Expand Down Expand Up @@ -111,6 +107,16 @@ const transferOption: ActionOptions<TransferContent> = {
suppressInitialMessage: true,
};

/**
* Check if a string is a valid UUID
* @param str The string to check
* @returns true if the string is a valid UUID
*/
function isUUID(str: string): boolean {
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
return uuidRegex.test(str);
}

/**
* Transfer action
*
Expand Down Expand Up @@ -150,7 +156,7 @@ export class TransferAction extends BaseFlowInjectableAction<TransferContent> {
async execute(
content: TransferContent | null,
_runtime: IAgentRuntime,
_message: Memory,
message: Memory,
_state?: State,
callback?: HandlerCallback,
) {
Expand All @@ -161,99 +167,107 @@ export class TransferAction extends BaseFlowInjectableAction<TransferContent> {

elizaLogger.log("Starting Flow Plugin's SEND_COIN handler...");

// Use shared wallet instance
// Use main account of the agent
const walletAddress = this.walletSerivce.address;
// TODO: use account pool transactions for the transfer action

const logPrefix = `Address: ${walletAddress}\n`;
// Get the user id
const userId = message.userId;
const isSelf = userId === message.agentId;
const logPrefix = `Account[${walletAddress}/${isSelf ? "root" : userId}]`;

// Parsed fields
const recipient = content.to;
const amount =
typeof content.amount === "number" ? content.amount : Number.parseFloat(content.amount);

// Check if the wallet has enough balance to transfer
const accountInfo = await defaultQueries.queryAccountBalanceInfo(
this.walletSerivce.wallet,
walletAddress,
);
const totalBalance = accountInfo.balance + (accountInfo.coaBalance ?? 0);
try {
let recipient = content.to;
// Check if the recipient is a user id
if (isUUID(content.to)) {
if (content.to === userId) {
// You can't send to yourself
throw new Error("Recipient is the same as the sender");
}

// Check if the amount is valid
if (totalBalance < amount) {
elizaLogger.error("Insufficient balance to transfer.");
callback?.({
text: `${logPrefix} Unable to process transfer request. Insufficient balance.`,
content: {
error: "Insufficient balance",
},
});
throw new Error("Insufficient balance to transfer");
}
// Get the wallet address of the user
const acctInfo = await this.acctPoolService.queryAccountInfo(content.to);
if (acctInfo) {
recipient = acctInfo.address;
elizaLogger.info(
`${logPrefix}\n Recipient is a user id - ${content.to}, its wallet address: ${recipient}`,
);
} else {
throw new Error(`Recipient not found with id: ${content.to}`);
}
}

let txId: string;
let keyIndex: number;
let txId: string;
let keyIndex: number;

try {
// For different token types, we need to handle the token differently
if (!content.token) {
elizaLogger.log(`${logPrefix} Sending ${amount} FLOW to ${recipient}...`);
// Check if the wallet has enough balance to transfer
const fromAccountInfo = await this.acctPoolService.queryAccountInfo(userId);
const totalBalance = fromAccountInfo.balance + (fromAccountInfo.coaBalance ?? 0);

// Check if the amount is valid
if (totalBalance < amount) {
throw new Error("Insufficient balance to transfer");
}

elizaLogger.log(`${logPrefix}\n Sending ${amount} FLOW to ${recipient}...`);
// Transfer FLOW token
const resp = await this.walletSerivce.sendTransaction(
transactions.mainFlowTokenDynamicTransfer,
(arg, t) => [arg(recipient, t.String), arg(amount.toFixed(1), t.UFix64)],
const resp = await this.acctPoolService.transferFlowToken(
userId,
recipient,
amount,
);
txId = resp.txId;
keyIndex = resp.index;
} else if (isCadenceIdentifier(content.token)) {
if (!isFlowAddress(recipient)) {
throw new Error("Recipient address is not a valid Flow address");
}

// Transfer Fungible Token on Cadence side
const [_, tokenAddr, tokenContractName] = content.token.split(".");
elizaLogger.log(
`${logPrefix} Sending ${amount} A.${tokenAddr}.${tokenContractName} to ${recipient}...`,
`${logPrefix}\n Sending ${amount} A.${tokenAddr}.${tokenContractName} to ${recipient}...`,
);
const resp = await this.walletSerivce.sendTransaction(
transactions.mainFTGenericTransfer,
(arg, t) => [
arg(amount.toFixed(1), t.UFix64),
arg(recipient, t.Address),
arg(`0x${tokenAddr}`, t.Address),
arg(tokenContractName, t.String),
],
const resp = await this.acctPoolService.transferGenericFT(
userId,
recipient,
amount,
`0x${tokenAddr}`,
tokenContractName,
);
txId = resp.txId;
keyIndex = resp.index;
} else if (isEVMAddress(content.token)) {
// Transfer ERC20 token on EVM side
// we need to update the amount to be in the smallest unit
const decimals = await defaultQueries.queryEvmERC20Decimals(
this.walletSerivce.wallet,
content.token,
);
const adjustedAmount = BigInt(amount * 10 ** decimals);
if (!isEVMAddress(recipient)) {
throw new Error("Recipient address is not a valid EVM address");
}

elizaLogger.log(
`${logPrefix} Sending ${adjustedAmount} ${content.token}(EVM) to ${recipient}...`,
`${logPrefix}\n Sending ${amount} ${content.token}(EVM) to ${recipient}...`,
);

const resp = await this.walletSerivce.sendTransaction(
transactions.mainEVMTransferERC20,
(arg, t) => [
arg(content.token, t.String),
arg(recipient, t.String),
// Convert the amount to string, the string should be pure number, not a scientific notation
arg(adjustedAmount.toString(), t.UInt256),
],
// Transfer ERC20 token on EVM side
const resp = await this.acctPoolService.transferERC20(
userId,
recipient,
amount,
content.token,
);
txId = resp.txId;
keyIndex = resp.index;
}

elizaLogger.log(`${logPrefix} Sent transaction: ${txId} by KeyIndex[${keyIndex}]`);
elizaLogger.log(`${logPrefix}\n Sent transaction: ${txId} by KeyIndex[${keyIndex}]`);

// call the callback with the transaction response
if (callback) {
const tokenName = content.token || "FLOW";
const extraMsg = `${logPrefix} Successfully transferred ${content.amount} ${tokenName} to ${content.to}`;
const extraMsg = `${logPrefix}\n Successfully transferred ${content.amount} ${tokenName} to ${content.to}`;
callback?.({
text: formatTransationSent(txId, this.walletSerivce.wallet.network, extraMsg),
content: {
Expand All @@ -268,7 +282,7 @@ export class TransferAction extends BaseFlowInjectableAction<TransferContent> {
} catch (e) {
elizaLogger.error("Error in sending transaction:", e.message);
callback?.({
text: `${logPrefix} Unable to process transfer request. Error in sending transaction.`,
text: `${logPrefix}\n Unable to process transfer request. Error: \n ${e.message}`,
content: {
error: e.message,
},
Expand Down
90 changes: 89 additions & 1 deletion packages/common/src/services/acctPool.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { injectable, inject } from "inversify";
import { elizaLogger, Service, type ServiceType, type IAgentRuntime } from "@elizaos/core";
import type { FlowAccountBalanceInfo } from "@elizaos/plugin-flow";
import { queries as defaultQueries, type FlowAccountBalanceInfo } from "@elizaos/plugin-flow";
import { globalContainer } from "@elizaos/plugin-di";
import {
FlowWalletService,
Expand Down Expand Up @@ -66,6 +66,8 @@ export class AccountsPoolService extends Service {
return this.walletService.address;
}

// ----- Flow blockchain READ scripts -----

/**
* Get the main account status
*/
Expand Down Expand Up @@ -145,6 +147,8 @@ export class AccountsPoolService extends Service {
return undefined;
}

// ----- Flow blockchain WRITE transactions -----

/**
* Create a new account
* @param userId
Expand All @@ -164,6 +168,90 @@ export class AccountsPoolService extends Service {
callbacks,
);
}

/**
* Transfer FlowToken to another account from the user's account
* @param fromUserId
*/
async transferFlowToken(
fromUserId: string,
recipient: string,
amount: number,
callbacks?: TransactionCallbacks,
): Promise<TransactionSentResponse> {
return await this.walletService.sendTransaction(
transactions.acctPoolFlowTokenDynamicTransfer,
(arg, t) => [
arg(recipient, t.String),
arg(amount.toFixed(8), t.UFix64),
arg(fromUserId, t.Optional(t.String)),
],
callbacks,
);
}

/**
* Transfer Cadence Generic FT to another account from the user's account
* @param fromUserId
* @param recipient
* @param amount
* @param tokenFTAddr
* @param tokenContractName
* @param callbacks
*/
async transferGenericFT(
fromUserId: string,
recipient: string,
amount: number,
tokenFTAddr: string,
tokenContractName: string,
callbacks?: TransactionCallbacks,
): Promise<TransactionSentResponse> {
return await this.walletService.sendTransaction(
transactions.acctPoolFTGenericTransfer,
(arg, t) => [
arg(amount.toFixed(8), t.UFix64),
arg(recipient, t.Address),
arg(tokenFTAddr, t.Address),
arg(tokenContractName, t.String),
arg(fromUserId, t.Optional(t.String)),
],
callbacks,
);
}

/**
* Transfer ERC20 token to another account from the user's account
* @param fromUserId
* @param recipient
* @param amount
* @param callback
*/
async transferERC20(
fromUserId: string,
recipient: string,
amount: number,
erc20Contract: string,
callbacks?: TransactionCallbacks,
): Promise<TransactionSentResponse> {
// Transfer ERC20 token on EVM side
// we need to update the amount to be in the smallest unit
const decimals = await defaultQueries.queryEvmERC20Decimals(
this.walletService.wallet,
erc20Contract,
);
const adjustedAmount = BigInt(amount * 10 ** decimals);
return await this.walletService.sendTransaction(
transactions.acctPoolEVMTransferERC20,
(arg, t) => [
arg(erc20Contract, t.String),
arg(recipient, t.String),
arg(adjustedAmount.toString(), t.UInt256),
arg(fromUserId, t.Optional(t.String)),
],
callbacks,
);
}
}

// Register the provider with the global container
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { TransactionStatus } from "@onflow/typedefs";
export interface ScriptQueryResponse {
ok: boolean;
data?: unknown;
error?: Record<string, unknown>;
error?: string | Record<string, unknown>;
errorMessage?: string;
}

Expand Down

0 comments on commit 631f973

Please sign in to comment.