Skip to content

Commit

Permalink
feat: Non-ETH token claim (#212)
Browse files Browse the repository at this point in the history
* feat: non-eth token claim

* UI fixes

* fix: eth fee estimation

* Change STRK to USDC
  • Loading branch information
ugur-eren authored Jul 15, 2024
1 parent add5ec3 commit de5b85a
Show file tree
Hide file tree
Showing 13 changed files with 246 additions and 63 deletions.
43 changes: 11 additions & 32 deletions JoyboyCommunity/src/constants/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ export type MultiChainTokens = Record<TokenSymbol, MultiChainToken>;

export enum TokenSymbol {
ETH = 'ETH',
/* STRK = 'STRK',
JBY = 'JBY', */
USDC = 'USDC',
}

export const ETH: MultiChainToken = {
Expand All @@ -34,48 +33,28 @@ export const ETH: MultiChainToken = {
},
};

/* export const STRK: MultiChainToken = {
export const USDC: MultiChainToken = {
[constants.StarknetChainId.SN_MAIN]: {
name: 'Stark',
symbol: TokenSymbol.STRK,
decimals: 18,
name: 'USD Coin',
symbol: TokenSymbol.USDC,
decimals: 6,
address: getChecksumAddress(
'0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d',
'0x53c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8',
),
},
[constants.StarknetChainId.SN_SEPOLIA]: {
name: 'Stark',
symbol: TokenSymbol.STRK,
decimals: 18,
name: 'USD Coin',
symbol: TokenSymbol.USDC,
decimals: 6,
address: getChecksumAddress(
'0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d',
'0x053b40a647cedfca6ca84f542a0fe36736031905a9639a7f19a3c1e66bfd5080',
),
},
};

export const JBY: MultiChainToken = {
[constants.StarknetChainId.SN_MAIN]: {
name: 'Joyboy',
symbol: TokenSymbol.JBY,
decimals: 18,
address: getChecksumAddress(
'0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7',
),
},
[constants.StarknetChainId.SN_SEPOLIA]: {
name: 'Joyboy',
symbol: TokenSymbol.JBY,
decimals: 18,
address: getChecksumAddress(
'0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7',
),
},
}; */

export const TOKENS: MultiChainTokens = {
[TokenSymbol.ETH]: ETH,
/* [TokenSymbol.STRK]: STRK,
[TokenSymbol.JBY]: JBY, */
[TokenSymbol.USDC]: USDC,
};

export const TOKEN_ADDRESSES: Record<
Expand Down
2 changes: 1 addition & 1 deletion JoyboyCommunity/src/hooks/useWindowDimensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {WEB_MAX_WIDTH} from '../constants/misc';
export const useWindowDimensions = () => {
const dimensions = useRNWindowDimensions();

if (Platform.OS === 'web') dimensions.width = WEB_MAX_WIDTH;
if (Platform.OS === 'web') dimensions.width = Math.min(dimensions.width, WEB_MAX_WIDTH);

return dimensions;
};
2 changes: 1 addition & 1 deletion JoyboyCommunity/src/screens/Auth/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ export const Login: React.FC<AuthLoginScreenProps> = ({navigation}) => {
description:
'Creating a new account will delete your current account. Are you sure you want to continue?',
buttons: [
{type: 'default', label: 'Cancel', onPress: hideDialog},
{
type: 'primary',
label: 'Continue',
Expand All @@ -75,6 +74,7 @@ export const Login: React.FC<AuthLoginScreenProps> = ({navigation}) => {
hideDialog();
},
},
{type: 'default', label: 'Cancel', onPress: hideDialog},
],
});
};
Expand Down
23 changes: 19 additions & 4 deletions JoyboyCommunity/src/screens/Tips/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,20 +40,35 @@ export const Tips: React.FC = () => {
const connectedAccount = await waitConnection();
if (!connectedAccount || !connectedAccount.address) return;

const deposit = await provider.callContract({
contractAddress: ESCROW_ADDRESSES[CHAIN_ID],
entrypoint: Entrypoint.GET_DEPOSIT,
calldata: [depositId],
});

if (deposit[0] === '0x0') {
showToast({
type: 'error',
title: 'This tip is not available anymore',
});
return;
}

const getNostrEvent = async (gasAmount: bigint) => {
const event = new NDKEvent(ndk);
event.kind = NDKKind.Text;
event.content = `claim: ${cairo.felt(depositId)},${cairo.felt(
connectedAccount.address!,

Check warning on line 61 in JoyboyCommunity/src/screens/Tips/index.tsx

View workflow job for this annotation

GitHub Actions / check-app

Forbidden non-null assertion
)},${cairo.felt(ETH[CHAIN_ID].address)},${gasAmount.toString()}`;
)},${cairo.felt(deposit[3])},${gasAmount.toString()}`;
event.tags = [];

await event.sign();
return event.rawEvent();
};

const feeResult = await estimateClaim.mutateAsync(await getNostrEvent(BigInt(1)));
const fee = BigInt(feeResult.data.fee);
const ethFee = BigInt(feeResult.data.ethFee);
const tokenFee = BigInt(feeResult.data.tokenFee);

const [balanceLow, balanceHigh] = await provider.callContract({
contractAddress: ETH[CHAIN_ID].address,
Expand All @@ -62,10 +77,10 @@ export const Tips: React.FC = () => {
});
const balance = uint256.uint256ToBN({low: balanceLow, high: balanceHigh});

if (balance < fee) {
if (balance < ethFee) {
// Send the claim through backend

const claimResult = await claim.mutateAsync(await getNostrEvent(fee));
const claimResult = await claim.mutateAsync(await getNostrEvent(tokenFee));
const txHash = claimResult.data.transaction_hash;

showTransactionModal(txHash, async (receipt) => {
Expand Down
3 changes: 3 additions & 0 deletions website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@
"lint": "next lint"
},
"dependencies": {
"@avnu/avnu-sdk": "^2.0.0",
"ethers": "^6.13.1",
"framer-motion": "^11.2.4",
"next": "^14.2.3",
"nostr-tools": "^2.7.0",
"qs": "^6.12.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"starknet": "6.9.0",
Expand Down
2 changes: 1 addition & 1 deletion website/src/app/api/deposit/calldata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,5 @@ export const getClaimCallData = async (data: (typeof ClaimSchema)['_output']) =>
uint256.bnToUint256(gasAmount),
]);

return {calldata, gasAmount};
return {calldata, gasAmount, tokenAddress};
};
70 changes: 62 additions & 8 deletions website/src/app/api/deposit/claim/route.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import {fetchBuildExecuteTransaction, fetchQuotes} from '@avnu/avnu-sdk';
import {NextRequest, NextResponse} from 'next/server';
import {Calldata} from 'starknet';

import {ESCROW_ADDRESSES} from '@/constants/contracts';
import {Entrypoint} from '@/constants/misc';
import {ESCROW_ADDRESSES, ETH_ADDRESSES} from '@/constants/contracts';
import {AVNU_URL, Entrypoint} from '@/constants/misc';
import {account} from '@/services/account';
import {provider} from '@/services/provider';
import {ErrorCode} from '@/utils/errors';
Expand All @@ -24,10 +25,12 @@ export async function POST(request: NextRequest) {

let claimCallData: Calldata;
let gasAmount: bigint;
let gasTokenAddress: string;
try {
const result = await getClaimCallData(body.data);
claimCallData = result.calldata;
gasAmount = result.gasAmount;
gasTokenAddress = result.tokenAddress;
} catch (error) {
if (error instanceof Error) {
return NextResponse.json({code: error.message}, {status: HTTPStatus.BadRequest});
Expand All @@ -37,18 +40,69 @@ export async function POST(request: NextRequest) {
}

try {
const {transaction_hash} = await account.execute(
[
if (gasTokenAddress === ETH_ADDRESSES[await provider.getChainId()]) {
// ETH transaction

const {transaction_hash} = await account.execute([
{
contractAddress: ESCROW_ADDRESSES[await provider.getChainId()],
entrypoint: Entrypoint.CLAIM,
calldata: claimCallData,
},
],
{maxFee: gasAmount},
);
]);

return NextResponse.json({transaction_hash}, {status: HTTPStatus.OK});
} else {
// ERC20 transaction

const result = await account.estimateInvokeFee([
{
contractAddress: ESCROW_ADDRESSES[await provider.getChainId()],
entrypoint: Entrypoint.CLAIM,
calldata: claimCallData,
},
]);

const gasFeeQuotes = await fetchQuotes(
{
buyTokenAddress: ETH_ADDRESSES[await provider.getChainId()],
sellTokenAddress: gasTokenAddress,
sellAmount: gasAmount,
},
{baseUrl: AVNU_URL},
);
const gasFeeQuote = gasFeeQuotes[0];

if (!gasFeeQuote) {
return NextResponse.json({code: ErrorCode.NO_ROUTE_FOUND}, {status: HTTPStatus.BadRequest});
}

if (result.overall_fee > gasFeeQuote.buyAmount) {
return NextResponse.json(
{code: ErrorCode.INVALID_GAS_AMOUNT},
{status: HTTPStatus.BadRequest},
);
}

return NextResponse.json({transaction_hash}, {status: HTTPStatus.OK});
const {calls: swapCalls} = await fetchBuildExecuteTransaction(
gasFeeQuote.quoteId,
account.address,
undefined,
undefined,
{baseUrl: AVNU_URL},
);

const {transaction_hash} = await account.execute([
{
contractAddress: ESCROW_ADDRESSES[await provider.getChainId()],
entrypoint: Entrypoint.CLAIM,
calldata: claimCallData,
},
...swapCalls,
]);

return NextResponse.json({transaction_hash}, {status: HTTPStatus.OK});
}
} catch (error) {
return NextResponse.json(
{code: ErrorCode.TRANSACTION_ERROR, error},
Expand Down
93 changes: 78 additions & 15 deletions website/src/app/api/deposit/estimate-claim/route.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import {fetchBuildExecuteTransaction, fetchQuotes} from '@avnu/avnu-sdk';
import {NextRequest, NextResponse} from 'next/server';
import {Calldata} from 'starknet';

import {ESCROW_ADDRESSES} from '@/constants/contracts';
import {Entrypoint} from '@/constants/misc';
import {ESCROW_ADDRESSES, ETH_ADDRESSES} from '@/constants/contracts';
import {AVNU_URL, Entrypoint} from '@/constants/misc';
import {account} from '@/services/account';
import {provider} from '@/services/provider';
import {ErrorCode} from '@/utils/errors';
Expand All @@ -23,9 +24,11 @@ export async function POST(request: NextRequest) {
}

let claimCallData: Calldata;
let gasTokenAddress: string;
try {
const {calldata} = await getClaimCallData(body.data);
const {calldata, tokenAddress} = await getClaimCallData(body.data);
claimCallData = calldata;
gasTokenAddress = tokenAddress;
} catch (error) {
if (error instanceof Error) {
return NextResponse.json({code: error.message}, {status: HTTPStatus.BadRequest});
Expand All @@ -35,18 +38,78 @@ export async function POST(request: NextRequest) {
}

try {
const result = await account.estimateInvokeFee([
{
contractAddress: ESCROW_ADDRESSES[await provider.getChainId()],
entrypoint: Entrypoint.CLAIM,
calldata: claimCallData,
},
]);

// Using 1.1 as a multiplier to ensure the fee is enough
const fee = ((result.overall_fee * BigInt(11)) / BigInt(10)).toString();

return NextResponse.json({fee}, {status: HTTPStatus.OK});
if (gasTokenAddress === ETH_ADDRESSES[await provider.getChainId()]) {
// ETH fee estimation

const result = await account.estimateInvokeFee([
{
contractAddress: ESCROW_ADDRESSES[await provider.getChainId()],
entrypoint: Entrypoint.CLAIM,
calldata: claimCallData,
},
]);

// Using 1.1 as a multiplier to ensure the fee is enough
const fee = ((result.overall_fee * BigInt(11)) / BigInt(10)).toString();

return NextResponse.json({ethFee: fee, tokenFee: fee}, {status: HTTPStatus.OK});
} else {
// ERC20 fee estimation

const quotes = await fetchQuotes(
{
sellTokenAddress: ETH_ADDRESSES[await provider.getChainId()],
buyTokenAddress: gasTokenAddress,
sellAmount: BigInt(1),
takerAddress: account.address,
},
{baseUrl: AVNU_URL},
);
const quote = quotes[0];

console.log(ETH_ADDRESSES[await provider.getChainId()], gasTokenAddress, quote);

if (!quote) {
return NextResponse.json({code: ErrorCode.NO_ROUTE_FOUND}, {status: HTTPStatus.BadRequest});
}

const {calls: swapCalls} = await fetchBuildExecuteTransaction(
quote.quoteId,
account.address,
undefined,
undefined,
{baseUrl: AVNU_URL},
);

const result = await account.estimateInvokeFee([
{
contractAddress: ESCROW_ADDRESSES[await provider.getChainId()],
entrypoint: Entrypoint.CLAIM,
calldata: claimCallData,
},
...swapCalls,
]);

// Using 1.1 as a multiplier to ensure the fee is enough
const ethFee = (result.overall_fee * BigInt(11)) / BigInt(10);

const feeQuotes = await fetchQuotes(
{
sellTokenAddress: ETH_ADDRESSES[await provider.getChainId()],
buyTokenAddress: gasTokenAddress,
sellAmount: ethFee,
takerAddress: account.address,
},
{baseUrl: AVNU_URL},
);
const feeQuote = feeQuotes[0];

if (!feeQuote) {
return NextResponse.json({code: ErrorCode.NO_ROUTE_FOUND}, {status: HTTPStatus.BadRequest});
}

return NextResponse.json({ethFee, tokenFee: feeQuote.buyAmount}, {status: HTTPStatus.OK});
}
} catch (error) {
return NextResponse.json(
{code: ErrorCode.ESTIMATION_ERROR, error},
Expand Down
7 changes: 7 additions & 0 deletions website/src/constants/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,10 @@ export const ESCROW_ADDRESSES = {
[constants.StarknetChainId.SN_SEPOLIA]:
'0x078a022e6906c83e049a30f7464b939b831ecbe47029480d7e89684f20c8d263',
};

export const ETH_ADDRESSES = {
[constants.StarknetChainId.SN_MAIN]:
'0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7',
[constants.StarknetChainId.SN_SEPOLIA]:
'0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7',
};
Loading

0 comments on commit de5b85a

Please sign in to comment.