Skip to content

Commit

Permalink
Merge pull request #9 from fixes-world/tbh/add-tokenlist-support
Browse files Browse the repository at this point in the history
feat(common): add tokenlist support
  • Loading branch information
btspoony authored Feb 7, 2025
2 parents 0c6ae9d + 004826c commit 98750fa
Show file tree
Hide file tree
Showing 13 changed files with 567 additions and 4 deletions.
32 changes: 32 additions & 0 deletions flow.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,14 @@
"testnet": "8c5303eaa26202d6"
}
},
"EVMTokenList": {
"source": "mainnet://15a918087ab12d86.EVMTokenList",
"hash": "d94f029ca11147e7d37f2d76ebb71a0c3f5e6c49a04df13a5717631a1d4be2d9",
"aliases": {
"mainnet": "15a918087ab12d86",
"testnet": "b86f928a1fa7798e"
}
},
"FTViewUtils": {
"source": "mainnet://15a918087ab12d86.FTViewUtils",
"hash": "ef8343697ebcb455a835bc9f87b8060f574c3d968644de47f6613cebf05d7749",
Expand Down Expand Up @@ -256,6 +264,22 @@
"testnet": "631e88ae7f1d7c20"
}
},
"NFTList": {
"source": "mainnet://15a918087ab12d86.NFTList",
"hash": "30bcd96975d4b175873fd516ebf58f64bfbf09e83c29c907abf98296e0347626",
"aliases": {
"mainnet": "15a918087ab12d86",
"testnet": "b86f928a1fa7798e"
}
},
"NFTViewUtils": {
"source": "mainnet://15a918087ab12d86.NFTViewUtils",
"hash": "9c0fe5d7ec4da744c1f7a2f2e5ce098879e86cd77e7e3c90ffb1b3974d797e1b",
"aliases": {
"mainnet": "15a918087ab12d86",
"testnet": "b86f928a1fa7798e"
}
},
"NonFungibleToken": {
"source": "mainnet://1d7e57aa55817448.NonFungibleToken",
"hash": "b63f10e00d1a814492822652dac7c0574428a200e4c26cb3c832c4829e2778f0",
Expand Down Expand Up @@ -377,6 +401,14 @@
"testnet": "b86f928a1fa7798e"
}
},
"TokenListHelper": {
"source": "mainnet://15a918087ab12d86.TokenListHelper",
"hash": "2bf02ca6b8e4499b6f7c81e2576e70108bea86a16e258a60a536946a728d5007",
"aliases": {
"mainnet": "15a918087ab12d86",
"testnet": "b86f928a1fa7798e"
}
},
"ViewResolver": {
"source": "mainnet://1d7e57aa55817448.ViewResolver",
"hash": "374a1994046bac9f6228b4843cb32393ef40554df9bd9907a702d098a2987bde",
Expand Down
346 changes: 346 additions & 0 deletions packages/common/src/actions/ensure-token-registered.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,346 @@
import { inject, injectable } from "inversify";
import { z } from "zod";
import {
elizaLogger,
type HandlerCallback,
type IAgentRuntime,
type Memory,
type State,
} from "@elizaos/core";
import { isCadenceIdentifier, isEVMAddress, type FlowAccountBalanceInfo } from "@elizaos/plugin-flow";
import { property, globalContainer, type ActionOptions } from "@elizaos/plugin-di";
import { BaseFlowInjectableAction, type TransactionCallbacks, type TransactionSentResponse } from "@fixes-ai/core";

import { scripts } from "../assets/scripts.defs";
import { formatFlowSpent, formatTransationSent, formatWalletCreated } from "../formater";
import { transactions } from "../assets/transactions.defs";

/**
* The generated content for the transfer action
*/
export class Content {
@property({
description:
"Cadence Resource Identifier or ERC20 contract address (if not native token).",
examples: [
"For Cadence resource identifier, the field should be 'A.1654653399040a61.ContractName'",
"For ERC20 contract address, the field should be '0xe6ffc15a5bde7dd33c127670ba2b9fcb82db971a'",
],
schema: z.string(),
})
token: string;

@property({
description:
"The blockchain VM type. This field should be either 'flow' or 'evm' according to the token type.",
examples: [
"If token field is Cadence resource identifier, the vm field should be 'flow'",
"If token field is ERC20 contract address, the vm field should be 'evm'",
],
schema: z.string().refine((vm) => ["flow", "evm"].includes(vm)),
})
vm: "flow" | "evm";

@property({
description:
"The bridging requirement. If user mentioned the token doesn't need to be bridged, set this field to false. Default is true.",
examples: [],
schema: z.boolean().default(true),
})
bridging: boolean;
}

/**
* The transfer action options
*/
const option: ActionOptions<Content> = {
name: "ENSURE_TOKEN_REGISTERED",
similes: [
"ENSURE_NFT_REGISTERED",
"REGISTER_TOKEN",
"REGISTER_NFT",
"REGISTER_FT",
],
description:
"Call this action to ensure any fungible token/coin or non-fungible token(NFT) be registered in the TokenList on Flow blockchain.",
examples: [
[
{
user: "{{user1}}",
content: {
text: "Register token A.1654653399040a61.FlowToken, no need to bridge",
action: "ENSURE_TOKEN_REGISTERED",
},
},
],
[
{
user: "{{user1}}",
content: {
text: "Register token 0xb73bf8e6a4477a952e0338e6cc00cc0ce5ad04ba to Tokenlist",
action: "ENSURE_TOKEN_REGISTERED",
},
},
],
],
contentClass: Content,
suppressInitialMessage: true,
};

/**
* Ensure token registered in TokenList
*
* @category Actions
* @description Ensure token registered in TokenList on Flow blockchain
*/
@injectable()
export class EnsureTokenRegisteredAction extends BaseFlowInjectableAction<Content> {
constructor() {
super(option);
}

/**
* Validate if the action can be executed
*/
async validate(_runtime: IAgentRuntime, message: Memory): Promise<boolean> {
if (!this.walletSerivce.isInitialized) {
return false;
}

const content =
typeof message.content === "string" ? message.content : message.content?.text;

if (!content) return false;

const keywords: string[] = ["token", "register", "tokenlist", "token-list", "nftlist", "nft-list"];
// Check if the message contains the keywords
return keywords.some((keyword) => content.toLowerCase().includes(keyword.toLowerCase()));
}

/**
* Execute the action
*
* @param content the content from processMessages
* @param callback the callback function to pass the result to Eliza runtime
* @returns the transaction response
*/
async execute(
content: Content | null,
runtime: IAgentRuntime,
message: Memory,
_state?: State,
callback?: HandlerCallback,
) {
if (!content) {
elizaLogger.warn("No content generated");
return;
}

elizaLogger.log(`Starting ${this.name} handler...`);

const userId = message.userId;
const isSelf = message.userId === runtime.agentId;
const mainAddr = this.walletSerivce.address;

const accountName = `Account[${mainAddr}/${isSelf ? "root" : userId}]`;

// Check if token is registered
let isRegistered = false;
let errorMsg: string | undefined = undefined;
let address: string;
let contractName: string;
if (isCadenceIdentifier(content.token) && content.vm === "flow") {
const [_, tokenAddr, tokenContractName] = content.token.split(".");
address = `0x${tokenAddr}`;
contractName = tokenContractName;

elizaLogger.debug(
`${accountName}\n Check A.${tokenAddr}.${tokenContractName} in TokenList...`,
);

try {
isRegistered = await this.walletSerivce.executeScript(scripts.isTokenRegistered, (arg, t) => [
arg(address, t.Address),
arg(contractName, t.String),
], false)
} catch (e) {
elizaLogger.error("Error in checking token registration:", e);
errorMsg = e.message;
}
} else if (isEVMAddress(content.token) && content.vm === "evm") {
elizaLogger.debug(
`${accountName}\n Check ${content.token} in EVMTokenList...`,
);
address = content.token;

try {
isRegistered = await this.walletSerivce.executeScript(scripts.isEVMAssetRegistered, (arg, t) => [
arg(content.token.toLowerCase(), t.String),
], false)
} catch (e) {
elizaLogger.error("Error in checking token registration:", e);
errorMsg = e.message;
}
} else {
errorMsg = `Invalid token format or wrong VM type: ${content.token} (${content.vm})`;
}

// if error occurred, return the error message
if (errorMsg) {
callback?.({
text: `Unable to fetch info for ${content.token}.`,
content: { error: errorMsg },
source: "FlowBlockchain",
});
return;
}

if (isRegistered) {
callback?.({
text: `Token ${content.token} is already registered in TokenList.`,
content: { exists: true },
source: "FlowBlockchain",
});
return;
}

type RegisterTokenResponse = {
success: boolean;
txid: string;
evmBridged: boolean;
from: string;
flowSpent: number;
gasFeeSpent: number;
}

// Register the token
try {
const resp = await new Promise<RegisterTokenResponse>((resolve, reject) => {
const transactionCallbacks: TransactionCallbacks = {
onFinalized: async (txId, status, errorMsg) => {
if (errorMsg) {
reject(new Error(`Error in the creation transaction: ${errorMsg}`));
return;
}

const validEventNames = [
'EVMTokenList.EVMBridgedAssetRegistered',
'TokenList.FungibleTokenRegistered',
'NFTList.NFTCollectionRegistered'
]
let fromAddress = "";
let flowSpent = 0;
let gasFeeSpent = 0;
let hasValidEvent = false;
let evmBridged = false;
for (const evt of status.events) {
// check if the transaction has a valid event
if (!hasValidEvent) {
const [_1, _2, contractName, eventName] = evt.type.split('.');
hasValidEvent = validEventNames.includes(`${contractName}.${eventName}`)
}
// check if the event is FlowToken.TokensWithdrawn from user's account
if (evt.type.endsWith('FlowToken.TokensWithdrawn') && evt.data.from !== this.walletSerivce.address) {
// calculate the flow spent
fromAddress = evt.data.from;
flowSpent += Number.parseFloat(evt.data.amount);
}
// check gas fee spent
if (evt.type.endsWith("FlowFees.FeesDeducted")) {
gasFeeSpent += Number.parseFloat(evt.data.amount);
}
// check if the event is FlowEVMBridge.BridgeDefiningContractDeployed
if (evt.type.endsWith("FlowEVMBridge.BridgeDefiningContractDeployed")) {
evmBridged = true;
}
}

if (hasValidEvent) {
elizaLogger.log(`Token registered successfully: ${content.token}`);
resolve({
success: true,
txid: txId,
evmBridged,
from: fromAddress,
flowSpent,
gasFeeSpent,
});
} else {
elizaLogger.log(`Failed to register token: ${content.token}, no valid event found.`);
resolve({
success: false,
txid: txId,
evmBridged,
from: fromAddress,
flowSpent,
gasFeeSpent,
});
}
},
}

// send the transaction to register the token, based on the VM type
let transaction: Promise<TransactionSentResponse>;

if (content.vm === "flow") {
if (content.bridging) {
transaction = this.walletSerivce.sendTransaction(
transactions.tlRegisterCadenceAsset,
(arg, t) => [
arg(address, t.Address),
arg(contractName, t.String),
arg(userId, t.String),
],
transactionCallbacks,
);
} else {
transaction = this.walletSerivce.sendTransaction(
transactions.tlRegisterCadenceAssetNoBridge,
(arg, t) => [
arg(address, t.Address),
arg(contractName, t.String),
],
transactionCallbacks,
)
}
} else {
transaction = this.walletSerivce.sendTransaction(
transactions.tlRegisterEVMAsset,
(arg, t) => [
arg(content.token, t.String),
arg(userId, t.String),
],
transactionCallbacks,
)
}
// wait for the transaction to be finalized
transaction.catch((e) => reject(e));
});
// format the flow spent information
const flowSpentInfo = formatFlowSpent(resp.from, resp.flowSpent, this.walletSerivce.address, resp.gasFeeSpent);
const prefix = `Operator: ${accountName}\n${flowSpentInfo}\n`;
// return the response to the callback
const finalMsg = resp.success
? `${prefix}\n Token ${content.token} registered successfully.`
: resp.evmBridged
? `${prefix}\n Token has just bridged from EVM side, you need send another transaction to register it in TokenList.`
: `${prefix}\n Failed to register token, no valid event found.`;
callback?.({
text: formatTransationSent(resp.txid, this.walletSerivce.connector.network, finalMsg),
content: resp,
source: "FlowBlockchain",
});
} catch (e) {
callback?.({
text: `Operator: ${accountName}\n Failed to register token, Error: ${e.message}`,
content: { error: e.message },
source: "FlowBlockchain",
});
}

elizaLogger.log(`Finished ${this.name} handler.`);
}
}

// Register the transfer action
globalContainer.bind(EnsureTokenRegisteredAction).toSelf();
Loading

0 comments on commit 98750fa

Please # to comment.