Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Non-ETH token claim #212

Merged
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
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 @@
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 @@
});
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
Loading