Skip to content

Commit

Permalink
frontend: integrate fee subsidy feature
Browse files Browse the repository at this point in the history
  • Loading branch information
GabrielBuragev committed Jul 10, 2023
1 parent a11be8b commit e60fe41
Show file tree
Hide file tree
Showing 13 changed files with 447 additions and 72 deletions.
1 change: 1 addition & 0 deletions frontend/src/components/RequestSourceInputs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ const { amount: requestFeeAmount, loading: requestFeeLoading } = useRequestFee(
computed(() => selectedSourceChain.value?.value.internalRpcUrl),
computed(() => selectedSourceChain.value?.value.requestManagerAddress),
selectedTokenAmount,
computed(() => selectedSourceChain.value?.value),
computed(() => props.targetChain?.value),
true,
);
Expand Down
20 changes: 14 additions & 6 deletions frontend/src/composables/useMaxTransferableTokenAmount.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Ref } from 'vue';
import { ref, watch } from 'vue';

import { amountCanBeSubsidized } from '@/services/transactions/fee-sub';
import { getAmountBeforeFees } from '@/services/transactions/request-manager';
import type { Chain } from '@/types/data';
import { TokenAmount } from '@/types/token-amount';
Expand All @@ -19,12 +20,19 @@ export function useMaxTransferableTokenAmount(
targetChain: Chain,
) {
try {
const transferableAmount = await getAmountBeforeFees(
balance,
sourceChain.internalRpcUrl,
sourceChain.requestManagerAddress,
targetChain.identifier,
);
const canBeSubsidized = await amountCanBeSubsidized(sourceChain, balance.token, balance);

let transferableAmount;
if (canBeSubsidized) {
transferableAmount = balance.uint256;
} else {
transferableAmount = await getAmountBeforeFees(
balance,
sourceChain.internalRpcUrl,
sourceChain.requestManagerAddress,
targetChain.identifier,
);
}
maxTransferableTokenAmount.value = TokenAmount.new(transferableAmount, balance.token);
} catch (e) {
maxTransferableTokenAmount.value = undefined;
Expand Down
24 changes: 19 additions & 5 deletions frontend/src/composables/useRequestFee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ import type { Ref } from 'vue';
import { ref, watch } from 'vue';

import { useDebouncedTask } from '@/composables/useDebouncedTask';
import { amountCanBeSubsidized } from '@/services/transactions/fee-sub';
import { getRequestFee } from '@/services/transactions/request-manager';
import type { Chain } from '@/types/data';
import { TokenAmount } from '@/types/token-amount';
import { UInt256 } from '@/types/uint-256';

export function useRequestFee(
rpcUrl: Ref<string | undefined>,
requestManagerAddress: Ref<string | undefined>,
requestAmount: Ref<TokenAmount | undefined>,
sourceChain: Ref<Chain | undefined>,
targetChain: Ref<Chain | undefined>,
debounced?: boolean,
debouncedDelay = 500,
Expand All @@ -26,20 +29,31 @@ export function useRequestFee(
!rpcUrl.value ||
!requestManagerAddress.value ||
!requestAmount.value ||
!targetChain.value
!targetChain.value ||
!sourceChain.value
) {
amount.value = undefined;
loading.value = false;
return;
}

try {
const requestFee = await getRequestFee(
rpcUrl.value,
requestManagerAddress.value,
const canBeSubsdized = await amountCanBeSubsidized(
sourceChain.value,
requestAmount.value.token,
requestAmount.value,
targetChain.value.identifier,
);
let requestFee;
if (canBeSubsdized) {
requestFee = new UInt256(0);
} else {
requestFee = await getRequestFee(
rpcUrl.value,
requestManagerAddress.value,
requestAmount.value,
targetChain.value.identifier,
);
}
amount.value = TokenAmount.new(requestFee, requestAmount.value.token);
} catch (exception: unknown) {
const errorMessage = (exception as { message?: string }).message;
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/composables/useTokenAllowance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export function useTokenAllowance(
provider.value,
token.value,
signerAddress.value,
sourceChain.value.requestManagerAddress,
sourceChain.value.feeSubAddress || sourceChain.value.requestManagerAddress,
);
} else {
allowance.value = undefined;
Expand Down
38 changes: 24 additions & 14 deletions frontend/src/composables/useTransferRequest.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { reactive } from 'vue';

import { Transfer } from '@/actions/transfers';
import { SubsidizedTransfer } from '@/actions/transfers/subsidized-transfer';
import { useAsynchronousTask } from '@/composables/useAsynchronousTask';
import { amountCanBeSubsidized } from '@/services/transactions/fee-sub';
import { getRequestFee } from '@/services/transactions/request-manager';
import type {
Eip1193Provider,
Expand Down Expand Up @@ -39,21 +41,29 @@ export function useTransferRequest() {
);
const fees = TokenAmount.new(requestFee, sourceTokenAmount.token);

const transfer = reactive(
Transfer.new(
options.sourceChain,
sourceTokenAmount,
options.targetChain,
targetTokenAmount,
options.toAddress,
validityPeriod,
fees,
options.approveInfiniteAmount,
options.requestCreatorAddress,
),
) as Transfer;
const transferData = [
options.sourceChain,
sourceTokenAmount,
options.targetChain,
targetTokenAmount,
options.toAddress,
validityPeriod,
fees,
options.approveInfiniteAmount,
options.requestCreatorAddress,
] as const;

let transfer;
if (
options.sourceChain.feeSubAddress &&
(await amountCanBeSubsidized(options.sourceChain, options.sourceToken, sourceTokenAmount))
) {
transfer = SubsidizedTransfer.new(...transferData);
} else {
transfer = Transfer.new(...transferData);
}

return transfer;
return reactive(transfer) as Transfer;
};

const execute = async (provider: IEthereumProvider, transfer: Transfer): Promise<void> => {
Expand Down
30 changes: 30 additions & 0 deletions frontend/src/services/transactions/fee-sub.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { getJsonRpcProvider, getReadOnlyContract } from '@/services/transactions/utils';
import type { Chain, Token } from '@/types/data';
import type { FeeSub } from '@/types/ethers-contracts';
import { FeeSub__factory } from '@/types/ethers-contracts';
import type { TokenAmount } from '@/types/token-amount';
import { UInt256 } from '@/types/uint-256';

export async function amountCanBeSubsidized(
chain: Chain,
token: Token,
tokenAmount: TokenAmount,
): Promise<boolean> {
if (!chain.feeSubAddress) {
return false;
}
const provider = getJsonRpcProvider(chain.internalRpcUrl);

const feeSubContract = getReadOnlyContract<FeeSub>(
chain.feeSubAddress,
FeeSub__factory.createInterface(),
provider,
);
const threshold = await feeSubContract.minimumAmounts(token.address);

if (threshold.isZero() || tokenAmount.uint256.lt(UInt256.parse(threshold.toString()))) {
return false;
}

return true;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { Ref } from 'vue';
import { ref } from 'vue';

import { useMaxTransferableTokenAmount } from '@/composables/useMaxTransferableTokenAmount';
import * as feeSubService from '@/services/transactions/fee-sub';
import * as requestManagerService from '@/services/transactions/request-manager';
import { TokenAmount } from '@/types/token-amount';
import { UInt256 } from '@/types/uint-256';
Expand All @@ -16,6 +17,7 @@ const TARGET_CHAIN_REF = ref(TARGET_CHAIN);
const TOKEN_AMOUNT = ref(new TokenAmount({ amount: '1000', token: TOKEN })) as Ref<TokenAmount>;

vi.mock('@/services/transactions/request-manager');
vi.mock('@/services/transactions/fee-sub');

describe('useMaxTransferableTokenAmount', () => {
beforeEach(() => {
Expand Down Expand Up @@ -49,27 +51,52 @@ describe('useMaxTransferableTokenAmount', () => {
expect(maxTransferableTokenAmount.value).toBeUndefined();
});

it('holds the actual transferable amount derived from the provided total amount', async () => {
const totalAmount = ref(
new TokenAmount({ amount: '1000', token: TOKEN }),
) as Ref<TokenAmount>;

const mockedAmountBeforeFees = totalAmount.value.uint256.subtract(new UInt256('100'));
Object.defineProperty(requestManagerService, 'getAmountBeforeFees', {
value: vi.fn().mockReturnValue(mockedAmountBeforeFees),
describe('if transfer can be subsidized', () => {
it('holds the full token balance as a transferable amount', async () => {
const totalAmount = ref(
new TokenAmount({ amount: '1000', token: TOKEN }),
) as Ref<TokenAmount>;

Object.defineProperty(feeSubService, 'amountCanBeSubsidized', {
value: vi.fn().mockReturnValue(true),
});

const { maxTransferableTokenAmount } = useMaxTransferableTokenAmount(
totalAmount,
SOURCE_CHAIN_REF,
TARGET_CHAIN_REF,
);
await flushPromises();

expect(maxTransferableTokenAmount.value).not.toBeUndefined();
expect(maxTransferableTokenAmount.value?.uint256.asString).toBe(
totalAmount.value.uint256.asString,
);
});
});
describe('if transfer cannot be subsidized', () => {
it('holds the actual transferable amount derived from the provided total amount', async () => {
const totalAmount = ref(
new TokenAmount({ amount: '1000', token: TOKEN }),
) as Ref<TokenAmount>;

const mockedAmountBeforeFees = totalAmount.value.uint256.subtract(new UInt256('100'));
Object.defineProperty(requestManagerService, 'getAmountBeforeFees', {
value: vi.fn().mockReturnValue(mockedAmountBeforeFees),
});

const { maxTransferableTokenAmount } = useMaxTransferableTokenAmount(
totalAmount,
SOURCE_CHAIN_REF,
TARGET_CHAIN_REF,
);
await flushPromises();

expect(maxTransferableTokenAmount.value).not.toBeUndefined();
expect(maxTransferableTokenAmount.value?.uint256.asString).toBe(
mockedAmountBeforeFees.asString,
);
});

const { maxTransferableTokenAmount } = useMaxTransferableTokenAmount(
totalAmount,
SOURCE_CHAIN_REF,
TARGET_CHAIN_REF,
);
await flushPromises();

expect(maxTransferableTokenAmount.value).not.toBeUndefined();
expect(maxTransferableTokenAmount.value?.uint256.asString).toBe(
mockedAmountBeforeFees.asString,
);
});

it('is undefined when calculation fails with an exception', async () => {
Expand Down
Loading

0 comments on commit e60fe41

Please sign in to comment.