Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
BITTE_API_KEY=''
NEAR_PK=''
NEXT_PUBLIC_ACCOUNT_ID=''
CRON_SECRET=''
BITTE_API_KEY=
NEAR_PK=
NEXT_PUBLIC_ACCOUNT_ID=
CRON_SECRET=
76 changes: 76 additions & 0 deletions app/api/withdraw/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { NextRequest, NextResponse } from "next/server";
import { BALANCE_UPDATE_DELAY, USDC_CONTRACT } from "@/lib/utils";
import {
initializeNearAccount,
depositUSDC,
getUSDCBalance,
intentsUSDCBalance,
withdrawUSDC,
withdrawToken,
intentsBalance,
} from "@/lib/near";
import { formatUnits } from "@/lib/viem";

const bigIntMin = (a: bigint, b: bigint) => (a < b ? a : b);
const ZERO = BigInt(0);

export async function GET(request: NextRequest) {
try {
if (process.env.CRON_SECRET) {
const authHeader = request.headers.get("Authorization");
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
}

const { searchParams } = new URL(request.url);
const withdrawStr = searchParams.get("amount");
if (!withdrawStr) {
return NextResponse.json(
{ error: "unspecified amount" },
{ status: 400 },
);
}
const token = searchParams.get("token") || USDC_CONTRACT;

const accountId = process.env.NEXT_PUBLIC_ACCOUNT_ID;
if (!accountId) {
return NextResponse.json(
{ error: "accountId is not configured" },
{ status: 500 },
);
}

const requestedWithdrawAmount = BigInt(withdrawStr);

const account = await initializeNearAccount(accountId);

const usdcBalance = await intentsBalance(account, token);
let withdrawAmount = bigIntMin(requestedWithdrawAmount, usdcBalance);

if (withdrawAmount == ZERO) {
return NextResponse.json(
{ message: "Nothing to withdraw" },
{ status: 200 },
);
}

const tx = await withdrawToken(account, token, withdrawAmount);

await new Promise((resolve) => setTimeout(resolve, BALANCE_UPDATE_DELAY));

const uiAmount = formatUnits(withdrawAmount, 6);

return NextResponse.json({
message: `Successfully withdrew $${uiAmount} USDC`,
transactionHash: tx.transaction.hash,
amount: uiAmount,
});
} catch (error) {
console.error("Error in deposit endpoint:", error);
return NextResponse.json(
{ error: "Failed to process deposit request" },
{ status: 500 },
);
}
}
86 changes: 80 additions & 6 deletions lib/near.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import { actionCreators } from "@near-js/transactions";
import { Account, KeyPair, keyStores, Near } from "near-api-js";
import type { Quote } from "./types";
import { INTENTS_CONTRACT_ID, NEAR_RPC_URL, TGas } from "./utils";
import {
INTENTS_CONTRACT_ID,
NEAR_RPC_URL,
TGas,
USDC_CONTRACT,
} from "./utils";
import { getEnvVar } from "./env";

const FIFTY_TGAS = BigInt(TGas * 50);
const ONE_YOCTO = BigInt(1);

export async function getTokenBalance(
account: Account,
assetId: string,
Expand Down Expand Up @@ -57,9 +65,6 @@ export function buildTransactionPayload(quote: Quote) {
};
}

const USDC_CONTRACT =
"17208628f84f5d6ad33f0da3bbbeb27ffcb398eac501a31bd6ad2011e36133a1";

export async function getUSDCBalance(account: Account): Promise<bigint> {
try {
const result = await account.viewFunction({
Expand All @@ -74,6 +79,27 @@ export async function getUSDCBalance(account: Account): Promise<bigint> {
}
}

export async function intentsUSDCBalance(account: Account): Promise<bigint> {
return intentsBalance(account, USDC_CONTRACT);
}

export async function intentsBalance(
account: Account,
token: string,
): Promise<bigint> {
try {
const result = await account.viewFunction({
contractId: INTENTS_CONTRACT_ID,
methodName: "mt_balance_of",
args: { token_id: `nep141:${token}`, account_id: account.accountId },
});
return BigInt(result as string);
} catch (error) {
console.warn("Failed to fetch USDC balance:", error);
return BigInt(0);
}
}

export async function depositUSDC(account: Account, amount: bigint) {
const result = await account.signAndSendTransaction({
receiverId: USDC_CONTRACT,
Expand All @@ -85,8 +111,8 @@ export async function depositUSDC(account: Account, amount: bigint) {
amount: amount.toString(),
msg: account.accountId,
},
BigInt(TGas * 50),
BigInt(1),
FIFTY_TGAS,
ONE_YOCTO,
),
],
});
Expand All @@ -109,3 +135,51 @@ export async function depositUSDC(account: Account, amount: bigint) {

return result;
}

export async function withdrawUSDC(account: Account, amount: bigint) {
return withdrawToken(account, USDC_CONTRACT, amount);
}

export async function withdrawToken(
account: Account,
token: string,
amount: bigint,
) {
const result = await account.signAndSendTransaction({
receiverId: INTENTS_CONTRACT_ID,
actions: [
actionCreators.functionCall(
"ft_withdraw",
{
token,
amount: amount.toString(),
receiver_id: account.accountId,
// Docs suggest refund is not necessarily possible if msg is specified!
// https://docs.near-intents.org/near-intents/market-makers/verifier/deposits-and-withdrawals/withdrawals#refunds-on-failed-withdrawals-warning
// msg: null
},
FIFTY_TGAS,
ONE_YOCTO,
),
],
});

const hasSuccess = result.receipts_outcome.some((receipt) =>
receipt.outcome.logs.some(
(log) => log.includes("ft_transfer") && log.includes(account.accountId),
),
);
const hasRefund = result.receipts_outcome.some((receipt) =>
receipt.outcome.logs.some(
(log) => log.includes("ft_transfer") && log.includes('"memo":"refund"'),
),
);

if (hasRefund || !hasSuccess) {
throw new Error(
`Withdraw failed - transaction was refunded ${result.transaction.hash}`,
);
}

return result;
}
2 changes: 2 additions & 0 deletions lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import type { AgentContext, Token, ToolResult } from "./types";

export const AGENT_ID = "trading-agent-kappa.vercel.app";

export const USDC_CONTRACT =
"17208628f84f5d6ad33f0da3bbbeb27ffcb398eac501a31bd6ad2011e36133a1";
export const INTENTS_CONTRACT_ID = "intents.near";
export const TGas = 1000000000000;
export const NEAR_RPC_URL = "https://free.rpc.fastnear.com";
Expand Down