Skip to content

Commit

Permalink
Merge pull request #21 from starc007/feat/constructor-credentials
Browse files Browse the repository at this point in the history
feat: allow passing credentials via constructor
Dhaiwat10 authored Dec 27, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
2 parents d8b72ab + e0e9b01 commit 26467d0
Showing 18 changed files with 201 additions and 137 deletions.
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -8,15 +8,18 @@ Docs: https://dhaiwatpandya.gitbook.io/fuel-agent-kit/
npm install fuel-agent-kit
```

Make sure you have the following environment variables set:
You will need two things:

- `OPENAI_API_KEY`: Your OpenAI API key
- `FUEL_WALLET_PRIVATE_KEY`: Your Fuel wallet private key
- An OpenAI API key
- A Fuel wallet private key

```ts
import { FuelAgent } from 'fuel-agent-kit';

const agent = new FuelAgent();
const agent = new FuelAgent({
openaiApiKey: process.env.OPENAI_API_KEY,
walletPrivateKey: process.env.FUEL_WALLET_PRIVATE_KEY,
});

// Call different functions
await agent.transfer({
46 changes: 34 additions & 12 deletions src/FuelAgent.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { agentExector } from './agent.js';
import { addLiquidity, type AddLiquidityParams } from './mira/addLiquidity.js';
import { swapExactInput, type SwapExactInputParams } from './mira/swap.js';
import { borrowAsset, type BorrowAssetParams } from './swaylend/borrow.js';
@@ -10,43 +9,66 @@ import {
transfer as walletTransfer,
type TransferParams,
} from './transfers/transfers.js';
import { createAgent } from './agent.js';
import { AgentExecutor } from 'langchain/agents';

interface FuelAgentConfig {
walletPrivateKey: string;
openAIKey: string;
}

export class FuelAgent {
constructor() {
if (!process.env.OPENAI_API_KEY) {
throw new Error('OPENAI_API_KEY is not set');
private walletPrivateKey: string;
private openAIKey: string;
private agentExecutor: AgentExecutor;

constructor(config: FuelAgentConfig) {
this.walletPrivateKey = config.walletPrivateKey;
this.openAIKey = config.openAIKey;

if (!this.openAIKey) {
throw new Error('OpenAI API key is required.');
}

if (!process.env.FUEL_WALLET_PRIVATE_KEY) {
throw new Error('FUEL_WALLET_PRIVATE_KEY is not set');
if (!this.walletPrivateKey) {
throw new Error('Fuel wallet private key is required.');
}

this.agentExecutor = createAgent(this.openAIKey);
}

getCredentials() {
return {
walletPrivateKey: this.walletPrivateKey,
openAIKey: this.openAIKey,
};
}

async execute(input: string) {
const response = await agentExector.invoke({
const response = await this.agentExecutor.invoke({
input,
});

return response;
}

async swapExactInput(params: SwapExactInputParams) {
return await swapExactInput(params);
return await swapExactInput(params, this.walletPrivateKey);
}

async transfer(params: TransferParams) {
return await walletTransfer(params);
return await walletTransfer(params, this.walletPrivateKey);
}

async supplyCollateral(params: SupplyCollateralParams) {
return await supplyCollateral(params);
return await supplyCollateral(params, this.walletPrivateKey);
}

async borrowAsset(params: BorrowAssetParams) {
return await borrowAsset(params);
return await borrowAsset(params, this.walletPrivateKey);
}

async addLiquidity(params: AddLiquidityParams) {
return await addLiquidity(params);
return await addLiquidity(params, this.walletPrivateKey);
}
}
29 changes: 11 additions & 18 deletions src/agent.ts
Original file line number Diff line number Diff line change
@@ -19,27 +19,20 @@ export const prompt = ChatPromptTemplate.fromMessages([
['placeholder', '{agent_scratchpad}'],
]);

export const getModel = () => {
export const createAgent = (openAIKey: string) => {
const model = new ChatOpenAI({
modelName: 'gpt-4o',
apiKey: process.env.OPENAI_API_KEY,
apiKey: openAIKey,
});

const boundModel = model.bindTools(tools);
const agent = createToolCallingAgent({
llm: model,
tools,
prompt,
});

return boundModel;
return new AgentExecutor({
agent,
tools,
});
};

export const agent = createToolCallingAgent({
llm: new ChatOpenAI({
modelName: 'gpt-4o',
apiKey: process.env.OPENAI_API_KEY,
}),
tools,
prompt,
});

export const agentExector = new AgentExecutor({
agent,
tools,
});
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const DEFAULT_SLIPPAGE = 0.01; // 1%
28 changes: 14 additions & 14 deletions src/mira/addLiquidity.ts
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@ import { bn, Provider } from 'fuels';
import { getAllVerifiedFuelAssets } from '../utils/assets.js';
import { buildPoolId, MiraAmm, ReadonlyMiraAmm } from 'mira-dex-ts';
import { setupWallet } from '../utils/setup.js';
import { DEFAULT_SLIPPAGE } from '../constants.js';

async function futureDeadline(provider: Provider) {
const block = await provider.getBlock('latest');
@@ -15,19 +16,20 @@ export type AddLiquidityParams = {
slippage?: number;
};

export const addLiquidity = async ({
amount0,
asset0Symbol,
asset1Symbol,
slippage = 0.01, // Default slippage of 1%
}: AddLiquidityParams) => {
export const addLiquidity = async (
params: AddLiquidityParams,
privateKey: string,
) => {
const { wallet, provider } = await setupWallet(privateKey);
const assets = await getAllVerifiedFuelAssets();

const asset0 = assets.find((asset) => asset.symbol === asset0Symbol);
const asset1 = assets.find((asset) => asset.symbol === asset1Symbol);
const asset0 = assets.find((asset) => asset.symbol === params.asset0Symbol);
const asset1 = assets.find((asset) => asset.symbol === params.asset1Symbol);

if (!asset0 || !asset1) {
throw new Error(`Asset ${asset0Symbol} or ${asset1Symbol} not found`);
throw new Error(
`Asset ${params.asset0Symbol} or ${params.asset1Symbol} not found`,
);
}

let isStable = asset0.symbol.includes('USD') && asset1.symbol.includes('USD');
@@ -48,9 +50,7 @@ export const addLiquidity = async ({
throw new Error('Invalid asset decimals');
}

const { provider, wallet } = await setupWallet();

const amount0InWei = bn.parseUnits(amount0, asset0Decimals);
const amount0InWei = bn.parseUnits(params.amount0, asset0Decimals);

const poolId = buildPoolId(asset0Id, asset1Id, isStable);

@@ -85,10 +85,10 @@ export const addLiquidity = async ({

// Calculate minimum amounts with slippage
const minAmount0 = amount0InWei
.mul(bn(100 - Math.floor(slippage * 100)))
.mul(bn(100 - Math.floor((params.slippage || DEFAULT_SLIPPAGE) * 100)))
.div(bn(100));
const minAmount1 = amount1InWei
.mul(bn(100 - Math.floor(slippage * 100)))
.mul(bn(100 - Math.floor((params.slippage || DEFAULT_SLIPPAGE) * 100)))
.div(bn(100));

console.log('Min Amount0 (Wei):', minAmount0.toString());
41 changes: 17 additions & 24 deletions src/mira/swap.ts
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ import { getAllVerifiedFuelAssets } from '../utils/assets.js';
import { buildPoolId, MiraAmm, ReadonlyMiraAmm } from 'mira-dex-ts';
import { setupWallet } from '../utils/setup.js';
import { getTxExplorerUrl } from '../utils/explorer.js';
import { DEFAULT_SLIPPAGE } from '../constants.js';

async function futureDeadline(provider: Provider) {
const block = await provider.getBlock('latest');
@@ -16,55 +17,47 @@ export type SwapExactInputParams = {
slippage?: number;
};

export const swapExactInput = async ({
amount,
fromSymbol,
toSymbol,
slippage = 0.01,
}: {
amount: string;
fromSymbol: string;
toSymbol: string;
slippage?: number;
}) => {
export const swapExactInput = async (
params: SwapExactInputParams,
privateKey: string,
) => {
const { wallet, provider } = await setupWallet(privateKey);
const assets = await getAllVerifiedFuelAssets();

const fromAsset = assets.find((asset) => asset.symbol === fromSymbol);
const toAsset = assets.find((asset) => asset.symbol === toSymbol);
const fromAsset = assets.find((asset) => asset.symbol === params.fromSymbol);
const toAsset = assets.find((asset) => asset.symbol === params.toSymbol);

if (!fromAsset) {
throw new Error(`Asset ${fromSymbol} not found`);
throw new Error(`Asset ${params.fromSymbol} not found`);
}

if (!toAsset) {
throw new Error(`Asset ${toSymbol} not found`);
throw new Error(`Asset ${params.toSymbol} not found`);
}

const fromAssetId = fromAsset?.assetId;
const toAssetId = toAsset?.assetId;

if (!fromAssetId) {
throw new Error(`Asset ${fromSymbol} not found`);
throw new Error(`Asset ${params.fromSymbol} not found`);
}

if (!toAssetId) {
throw new Error(`Asset ${toSymbol} not found`);
throw new Error(`Asset ${params.toSymbol} not found`);
}

const fromAssetDecimals = fromAsset?.decimals;
const toAssetDecimals = toAsset?.decimals;

if (!fromAssetDecimals) {
throw new Error(`Asset ${fromSymbol} not found`);
throw new Error(`Asset ${params.fromSymbol} not found`);
}

if (!toAssetDecimals) {
throw new Error(`Asset ${toSymbol} not found`);
throw new Error(`Asset ${params.toSymbol} not found`);
}

const { wallet, provider } = await setupWallet();

const amountInWei = bn.parseUnits(amount, fromAssetDecimals);
const amountInWei = bn.parseUnits(params.amount, fromAssetDecimals);

let isStable =
fromAsset.symbol.includes('USD') && toAsset.symbol.includes('USD');
@@ -102,7 +95,7 @@ export const swapExactInput = async ({
console.log('Estimated Amount Out (Wei):', amountOutWei.toString());

const minAmountOut = amountOutWei
.mul(bn(100 - Math.floor(slippage * 100)))
.mul(bn(100 - Math.floor((params.slippage || DEFAULT_SLIPPAGE) * 100)))
.div(bn(100));

const req = await miraAmm.swapExactInput(
@@ -123,5 +116,5 @@ export const swapExactInput = async ({

const { id, status } = await tx.waitForResult();

return `Successfully swapped ${amount} ${fromSymbol} for ${toSymbol}. Explorer link: ${getTxExplorerUrl(id)}`;
return `Successfully swapped ${params.amount} ${params.fromSymbol} for ${params.toSymbol}. Explorer link: ${getTxExplorerUrl(id)}`;
};
13 changes: 8 additions & 5 deletions src/swaylend/borrow.ts
Original file line number Diff line number Diff line change
@@ -10,8 +10,11 @@ export type BorrowAssetParams = {
amount: string;
};

export const borrowAsset = async ({ amount }: BorrowAssetParams) => {
const { wallet, provider } = await setupWallet();
export const borrowAsset = async (
params: BorrowAssetParams,
privateKey: string,
) => {
const { wallet, provider } = await setupWallet(privateKey);

const marketContractId =
'0x657ab45a6eb98a4893a99fd104347179151e8b3828fd8f2a108cc09770d1ebae';
@@ -21,7 +24,7 @@ export const borrowAsset = async ({ amount }: BorrowAssetParams) => {
const asset = allAssets.find((asset) => asset.symbol === 'USDC'); // We can only borrow USDC
const assetId: any = asset?.assetId;

const weiAmount = bn.parseUnits(amount, asset?.decimals);
const weiAmount = bn.parseUnits(params.amount, asset?.decimals);

// fetch configs
const { value: marketConfiguration } = await marketContract.functions
@@ -77,7 +80,7 @@ export const borrowAsset = async ({ amount }: BorrowAssetParams) => {
};

const { waitForResult } = await marketContract.functions
.withdraw_base((+amount).toFixed(0), priceUpdateData)
.withdraw_base((+params.amount).toFixed(0), priceUpdateData)
.callParams({
forward: {
amount: fee,
@@ -91,5 +94,5 @@ export const borrowAsset = async ({ amount }: BorrowAssetParams) => {
const transactionResult = await waitForResult();

// Return the transaction ID
return `Successfully borrowed ${amount} USDC. Explorer link: ${getTxExplorerUrl(transactionResult.transactionId)}`;
return `Successfully borrowed ${params.amount} USDC. Explorer link: ${getTxExplorerUrl(transactionResult.transactionId)}`;
};
16 changes: 8 additions & 8 deletions src/swaylend/supply.ts
Original file line number Diff line number Diff line change
@@ -9,21 +9,21 @@ export type SupplyCollateralParams = {
symbol: string;
};

export const supplyCollateral = async ({
amount,
symbol,
}: SupplyCollateralParams) => {
const { wallet } = await setupWallet();
export const supplyCollateral = async (
params: SupplyCollateralParams,
privateKey: string,
) => {
const { wallet } = await setupWallet(privateKey);

const marketContractId =
'0x657ab45a6eb98a4893a99fd104347179151e8b3828fd8f2a108cc09770d1ebae';
const marketContract: Market = new Market(marketContractId, wallet);

const allAssets = await getAllVerifiedFuelAssets();
const asset = allAssets.find((asset) => asset.symbol === symbol);
const asset = allAssets.find((asset) => asset.symbol === params.symbol);
const assetId = asset?.assetId;

const weiAmount = bn.parseUnits(amount, asset?.decimals);
const weiAmount = bn.parseUnits(params.amount, asset?.decimals);

const tx = await marketContract.functions
.supply_collateral()
@@ -37,5 +37,5 @@ export const supplyCollateral = async ({

const { transactionId } = await tx.waitForResult();

return `Successfully supplied ${amount} ${symbol} as collateral. Explorer link: ${getTxExplorerUrl(transactionId)}`;
return `Successfully supplied ${params.amount} ${params.symbol} as collateral. Explorer link: ${getTxExplorerUrl(transactionId)}`;
};
38 changes: 26 additions & 12 deletions src/tools.ts
Original file line number Diff line number Diff line change
@@ -6,7 +6,18 @@ import { supplyCollateral } from './swaylend/supply.js';
import { borrowAsset } from './swaylend/borrow.js';
import { addLiquidity } from './mira/addLiquidity.js';

export const transferTool = tool(transfer, {
/**
* Wrapper functions that only take the params argument
* @param fn - The function to wrap.
* @returns A new function that takes only the params argument.
*/
const wrapWithoutPrivateKey = <T>(
fn: (params: T, privateKey: string) => Promise<any>,
) => {
return (params: T) => fn(params, '');
};

export const transferTool = tool(wrapWithoutPrivateKey(transfer), {
name: 'fuel_transfer',
description: 'Transfer any verified Fuel asset to another wallet',
schema: z.object({
@@ -16,7 +27,7 @@ export const transferTool = tool(transfer, {
}),
});

export const swapExactInputTool = tool(swapExactInput, {
export const swapExactInputTool = tool(wrapWithoutPrivateKey(swapExactInput), {
name: 'swap_exact_input',
description: 'Swap exact input on Mira',
schema: z.object({
@@ -32,24 +43,27 @@ export const swapExactInputTool = tool(swapExactInput, {
}),
});

export const supplyCollateralTool = tool(supplyCollateral, {
name: 'supply_collateral',
description: 'Supply collateral on swaylend',
schema: z.object({
amount: z.string().describe('The amount to lend'),
symbol: z.string().describe('The asset symbol to lend. eg. USDC, ETH'),
}),
});
export const supplyCollateralTool = tool(
wrapWithoutPrivateKey(supplyCollateral),
{
name: 'supply_collateral',
description: 'Supply collateral on swaylend',
schema: z.object({
amount: z.string().describe('The amount to lend'),
symbol: z.string().describe('The asset symbol to lend. eg. USDC, ETH'),
}),
},
);

export const borrowAssetTool = tool(borrowAsset, {
export const borrowAssetTool = tool(wrapWithoutPrivateKey(borrowAsset), {
name: 'borrow_asset',
description: 'Borrow asset on swaylend',
schema: z.object({
amount: z.string().describe('The amount to borrow'),
}),
});

export const addLiquidityTool = tool(addLiquidity, {
export const addLiquidityTool = tool(wrapWithoutPrivateKey(addLiquidity), {
name: 'add_liquidity',
description: 'Add liquidity to a Mira pool',
schema: z.object({
18 changes: 7 additions & 11 deletions src/transfers/transfers.ts
Original file line number Diff line number Diff line change
@@ -9,24 +9,20 @@ export type TransferParams = {
symbol: string;
};

export const transfer = async ({
to,
amount, // eg. 0.2
symbol,
}: TransferParams) => {
const { wallet } = await setupWallet();
export const transfer = async (params: TransferParams, privateKey: string) => {
const { wallet } = await setupWallet(privateKey);

const allAssets = await getAllVerifiedFuelAssets();
const asset = allAssets.find((asset) => asset.symbol === symbol);
const asset = allAssets.find((asset) => asset.symbol === params.symbol);
const assetId = asset?.assetId;

if (!assetId) {
throw new Error(`Asset ${symbol} not found`);
throw new Error(`Asset ${params.symbol} not found`);
}

const response = await wallet.transfer(
to,
bn.parseUnits(amount, asset.decimals),
params.to,
bn.parseUnits(params.amount, asset.decimals),
assetId,
);
const { id, isStatusFailure } = await response.waitForResult();
@@ -35,5 +31,5 @@ export const transfer = async ({
console.error('TX failed');
}

return `Sucessfully transferred ${amount}${symbol} to ${to}. Explorer link: ${getTxExplorerUrl(id)}`;
return `Sucessfully transferred ${params.amount} ${params.symbol} to ${params.to}. Explorer link: ${getTxExplorerUrl(id)}`;
};
8 changes: 3 additions & 5 deletions src/utils/setup.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { Provider, Wallet } from 'fuels';

export const setupWallet = async () => {
export const setupWallet = async (privateKey: string) => {
const provider = await Provider.create(
'https://mainnet.fuel.network/v1/graphql',
);
const wallet = Wallet.fromPrivateKey(
process.env.FUEL_WALLET_PRIVATE_KEY as string,
provider,
);

const wallet = Wallet.fromPrivateKey(privateKey, provider);

return {
wallet,
16 changes: 10 additions & 6 deletions test/add_liquidity.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { test } from 'vitest';
import { addLiquidity } from '../src/mira/addLiquidity.js';
import { FuelAgent } from '../src/FuelAgent.js';
import { test, beforeEach } from 'vitest';
import { createTestAgent, type FuelAgentType } from './setup.js';

let agent: FuelAgentType;

beforeEach(() => {
agent = createTestAgent();
});

test(
'add liquidity',
@@ -9,7 +14,7 @@ test(
},
async () => {
console.log(
await addLiquidity({
await agent.addLiquidity({
amount0: '0.0001',
asset0Symbol: 'ETH',
asset1Symbol: 'USDT',
@@ -19,12 +24,11 @@ test(
);

test(
'add 0.1 USDC liquidity to USDC/USDT pool',
'add liquidity via natural language',
{
timeout: 60000,
},
async () => {
const agent = new FuelAgent();
console.log(
await agent.execute(
'Add liquidity into USDC/USDT pool for 0.1 USDC with 5% slippage',
12 changes: 9 additions & 3 deletions test/borrow.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { test } from 'vitest';
import { FuelAgent } from '../dist/index.js';
import { test, beforeEach } from 'vitest';
import { FuelAgent } from '../src/FuelAgent.js';
import { createTestAgent } from './setup.js';

let agent: FuelAgent;

beforeEach(() => {
agent = createTestAgent();
});

test(
'borrowAsset',
async () => {
const agent = new FuelAgent();
console.log(await agent.execute('Borrow 11 USDC'));
},
{
11 changes: 8 additions & 3 deletions test/deposit.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { test } from 'vitest';
import { FuelAgent } from '../dist/index.js';
import { test, beforeEach } from 'vitest';
import { createTestAgent, type FuelAgentType } from './setup.js';

let agent: FuelAgentType;

beforeEach(() => {
agent = createTestAgent();
});

test(
'supplyCollateral',
async () => {
const agent = new FuelAgent();
console.log(await agent.execute('Supply 2 USDT as collateral'));
},
{
11 changes: 8 additions & 3 deletions test/execute.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { test } from 'vitest';
import { FuelAgent } from '../dist/index.js';
import { test, beforeEach } from 'vitest';
import { createTestAgent, type FuelAgentType } from './setup.js';

let agent: FuelAgentType;

beforeEach(() => {
agent = createTestAgent();
});

test(
'execute swap',
async () => {
const agent = new FuelAgent();
console.log(await agent.execute('swap 5 usdc for eth'));
},
{
12 changes: 12 additions & 0 deletions test/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { FuelAgent } from '../src/FuelAgent.js';

export type FuelAgentType = FuelAgent;

export const TEST_CREDENTIALS = {
walletPrivateKey: process.env.FUEL_WALLET_PRIVATE_KEY!,
openAIKey: process.env.OPENAI_API_KEY!,
};

export const createTestAgent = () => {
return new FuelAgent(TEST_CREDENTIALS);
};
16 changes: 10 additions & 6 deletions test/swap.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { test } from 'vitest';
import { swapExactInput } from '../src/mira/swap.js';
import { FuelAgent } from '../src/FuelAgent.js';
import { test, beforeEach } from 'vitest';
import { createTestAgent, type FuelAgentType } from './setup.js';

let agent: FuelAgentType;

beforeEach(() => {
agent = createTestAgent();
});

test(
'swap exact input',
@@ -9,7 +14,7 @@ test(
},
async () => {
console.log(
await swapExactInput({
await agent.swapExactInput({
amount: '1',
fromSymbol: 'USDC',
toSymbol: 'ETH',
@@ -19,12 +24,11 @@ test(
);

test(
'add 0.1 USDC liquidity to USDC/USDT pool',
'swap via natural language',
{
timeout: 60000,
},
async () => {
const agent = new FuelAgent();
console.log(await agent.execute('Swap 0.1 USDC to ETH with 5% slippage'));
},
);
11 changes: 8 additions & 3 deletions test/transfer.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { test } from 'vitest';
import { FuelAgent } from '../src/FuelAgent.js';
import { test, beforeEach } from 'vitest';
import { createTestAgent, type FuelAgentType } from './setup.js';

let agent: FuelAgentType;

beforeEach(() => {
agent = createTestAgent();
});

test(
'transfer USDC to another wallet',
async () => {
const agent = new FuelAgent();
console.log(
await agent.execute(
'Transfer 2 USDC to 0x8F8afB12402C9a4bD9678Bec363E51360142f8443FB171655eEd55dB298828D1',

0 comments on commit 26467d0

Please sign in to comment.