Skip to content

Commit 5a01099

Browse files
committed
Add transfer-ft primitive
1 parent beb6940 commit 5a01099

File tree

4 files changed

+291
-4
lines changed

4 files changed

+291
-4
lines changed

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@
2727
"dependencies": {
2828
"@mintbase-js/data": "^0.6.5",
2929
"@mintbase-js/rpc": "^0.6.5",
30+
"@mintbase-js/sdk": "^0.6.5",
3031
"@near-wallet-selector/core": "^8.9.15",
3132
"ai": "^4.1.0",
33+
"fuse.js": "^7.0.0",
3234
"near-safe": "^0.9.9",
3335
"openai": "^4.79.1",
3436
"viem": "^2.22.11",

pnpm-lock.yaml

+24-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/primitives/generate-transaction.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { FTS_METADATA } from '../data';
1717
const FINANCIAL_TRANSACTION_METHODS = ['ft_transfer'];
1818
const WARNING_PRICE = 100; // $100 USD
1919

20-
type GenerateTransactionBuilderParams = {
20+
export type GenerateTransactionBuilderParams = {
2121
nearRpcUrl: string;
2222
nearNetworkId: NearNetworkId;
2323
};

src/primitives/transfer-fts.ts

+264
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
import { ftStorageBalance } from '@mintbase-js/rpc';
2+
import { GAS_CONSTANTS, ONE_YOCTO } from '@mintbase-js/sdk';
3+
import { FunctionCallAction } from '@near-wallet-selector/core';
4+
import { parseUnits } from 'viem';
5+
6+
import {
7+
AccountTransaction,
8+
AllowlistedToken,
9+
BitteToolBuilder,
10+
BitteToolResult,
11+
NearNetworkId,
12+
} from '../types';
13+
import {
14+
GenerateTransactionBuilderParams,
15+
generateTransactionPrimitive,
16+
} from './generate-transaction';
17+
import { getErrorMsg } from '../utils';
18+
import { FTS_METADATA } from '../data';
19+
import Fuse from 'fuse.js';
20+
21+
type TransferFtParams = {
22+
signerId: string;
23+
receiverId: string;
24+
tokenIdentifier: string;
25+
amount: string;
26+
network?: 'mainnet' | 'testnet';
27+
};
28+
29+
type TransferFtResult = {
30+
transactions: AccountTransaction[];
31+
warnings?: string[];
32+
};
33+
34+
export const transferFtPrimitive: BitteToolBuilder<
35+
GenerateTransactionBuilderParams,
36+
TransferFtParams,
37+
TransferFtResult
38+
> = (args: GenerateTransactionBuilderParams) => ({
39+
toolSpec: {
40+
function: {
41+
name: 'transfer-ft',
42+
description:
43+
'Transfer fungible tokens. Automates the process of transferring fungible tokens and renders a Near transaction payload for the user to review and sign.',
44+
parameters: {
45+
type: 'object',
46+
required: ['signerId', 'receiverId', 'tokenIdentifier', 'amount'],
47+
properties: {
48+
signerId: {
49+
type: 'string',
50+
description:
51+
'Address of the account sending the fungible token amount.',
52+
},
53+
receiverId: {
54+
type: 'string',
55+
description:
56+
'Address of the account to receive the fungible token amount.',
57+
},
58+
tokenIdentifier: {
59+
type: 'string',
60+
description:
61+
'Name, symbol, or contract ID of the fungible token being transferred. This will be used in a fuzzy search to find the token.',
62+
},
63+
amount: {
64+
type: 'string',
65+
description:
66+
"Amount of tokens to be transferred. Example: '1' for 1 token, '0.001' for 0.001 tokens.",
67+
},
68+
network: {
69+
type: 'string',
70+
description: 'The NEAR network on which the transfer will occur.',
71+
enum: ['mainnet', 'testnet'],
72+
},
73+
},
74+
},
75+
},
76+
type: 'function',
77+
},
78+
execute: async ({
79+
signerId,
80+
receiverId,
81+
tokenIdentifier,
82+
amount,
83+
network = 'mainnet',
84+
}): Promise<BitteToolResult<TransferFtResult>> => {
85+
try {
86+
const generateTransaction = generateTransactionPrimitive(args);
87+
88+
const transactions: AccountTransaction[] = [];
89+
const warnings: string[] = [];
90+
91+
const tokenInfo = searchToken(tokenIdentifier, network)?.[0];
92+
93+
if (!tokenInfo) {
94+
return {
95+
error: `Token '${tokenIdentifier}' not found. Please check the token name and try again.`,
96+
};
97+
}
98+
99+
const contractId = tokenInfo.contractId;
100+
101+
const txArgs = {
102+
receiver_id: receiverId,
103+
amount: parseUnits(amount, tokenInfo.decimals).toString(),
104+
memo: null,
105+
};
106+
107+
const isUserRegistered = await ftStorageBalance({
108+
contractId,
109+
accountId: receiverId,
110+
rpcUrl: args.nearRpcUrl,
111+
});
112+
113+
// Check and build storage_deposit transaction if needed
114+
if (!isUserRegistered) {
115+
const storageDeposit: FunctionCallAction = {
116+
type: 'FunctionCall',
117+
params: {
118+
methodName: 'storage_deposit',
119+
args: { account_id: receiverId },
120+
deposit: ONE_YOCTO,
121+
gas: GAS_CONSTANTS.DEFAULT_GAS,
122+
},
123+
};
124+
125+
const { data: storageDepositData, error: storageDepositError } =
126+
await generateTransaction.execute({
127+
transactions: [
128+
{
129+
signerId,
130+
receiverId: contractId,
131+
actions: [storageDeposit],
132+
},
133+
],
134+
network,
135+
});
136+
137+
if (
138+
storageDepositError ||
139+
!storageDepositData ||
140+
storageDepositData.transactions.length === 0
141+
) {
142+
return {
143+
error:
144+
storageDepositError ||
145+
'Error generating storage_deposit transaction',
146+
};
147+
}
148+
149+
transactions.push(...storageDepositData.transactions);
150+
if (storageDepositData.warnings) {
151+
warnings.push(...storageDepositData.warnings);
152+
}
153+
}
154+
155+
// Build ft_transfer transaction
156+
const ftTransfer: FunctionCallAction = {
157+
type: 'FunctionCall',
158+
params: {
159+
methodName: 'ft_transfer',
160+
args: txArgs,
161+
deposit: ONE_YOCTO,
162+
gas: GAS_CONSTANTS.FT_TRANSFER,
163+
},
164+
};
165+
const { data: ftTransferData, error: ftTransferError } =
166+
await generateTransaction.execute({
167+
transactions: [
168+
{
169+
signerId,
170+
receiverId: contractId,
171+
actions: [ftTransfer],
172+
},
173+
],
174+
network,
175+
});
176+
177+
if (
178+
ftTransferError ||
179+
!ftTransferData ||
180+
ftTransferData.transactions.length === 0
181+
) {
182+
return {
183+
error: ftTransferError || 'Error generating ft_transfer transaction',
184+
};
185+
}
186+
187+
transactions.push(...ftTransferData.transactions);
188+
if (ftTransferData.warnings) {
189+
warnings.push(...ftTransferData.warnings);
190+
}
191+
192+
return {
193+
data: {
194+
transactions,
195+
warnings: warnings.length > 0 ? warnings : undefined,
196+
},
197+
};
198+
} catch (error) {
199+
return {
200+
error: getErrorMsg(error),
201+
};
202+
}
203+
},
204+
});
205+
206+
const tokens = ((): {
207+
mainnet: AllowlistedToken[];
208+
testnet: AllowlistedToken[];
209+
} => {
210+
const mainnet: AllowlistedToken[] = [];
211+
const testnet: AllowlistedToken[] = [];
212+
213+
for (const token of FTS_METADATA) {
214+
const { name, symbol, decimals, icon } = token;
215+
216+
if (token.mainnet) {
217+
mainnet.push({
218+
name,
219+
symbol,
220+
decimals,
221+
icon,
222+
contractId: token.mainnet,
223+
});
224+
}
225+
226+
if (token.testnet) {
227+
testnet.push({
228+
name,
229+
symbol,
230+
decimals,
231+
icon,
232+
contractId: token.testnet,
233+
});
234+
}
235+
}
236+
return { mainnet, testnet };
237+
})();
238+
239+
export const searchToken = (
240+
query: string,
241+
network: NearNetworkId = 'mainnet',
242+
): AllowlistedToken[] | null => {
243+
const isMainnet = network === 'mainnet';
244+
if (query.toLowerCase() === 'near') {
245+
query = isMainnet ? 'wrap.near' : 'wrap.testnet'; // Special case for NEAR
246+
}
247+
const fuse = new Fuse(isMainnet ? tokens.mainnet : tokens.testnet, {
248+
keys: [
249+
{ name: 'name', weight: 0.5 },
250+
{ name: 'symbol', weight: 0.4 },
251+
{ name: 'contractId', weight: 0.1 },
252+
],
253+
isCaseSensitive: false,
254+
threshold: 0.0,
255+
});
256+
257+
const result = fuse.search(query);
258+
259+
if (result.length === 0) {
260+
return null;
261+
}
262+
263+
return result.map((item) => item.item);
264+
};

0 commit comments

Comments
 (0)