diff --git a/apps/trading/client-pages/referrals/hooks/use-referral.ts b/apps/trading/client-pages/referrals/hooks/use-referral.ts
index f8fce1adb..cc8c4bc8b 100644
--- a/apps/trading/client-pages/referrals/hooks/use-referral.ts
+++ b/apps/trading/client-pages/referrals/hooks/use-referral.ts
@@ -14,7 +14,7 @@ export const DEFAULT_AGGREGATION_DAYS = 30;
export type Role = 'referrer' | 'referee';
type UseReferralArgs = (
- | { code: string }
+ | { code: string | undefined }
| { pubKey: string | null; role: Role }
) & {
aggregationEpochs?: number;
diff --git a/apps/trading/client-pages/referrals/referral-statistics.spec.tsx b/apps/trading/client-pages/referrals/referral-statistics.spec.tsx
index 0a3d41a84..be2f08c33 100644
--- a/apps/trading/client-pages/referrals/referral-statistics.spec.tsx
+++ b/apps/trading/client-pages/referrals/referral-statistics.spec.tsx
@@ -1,6 +1,9 @@
import { MockedProvider, type MockedResponse } from '@apollo/react-testing';
-import { render, waitFor } from '@testing-library/react';
-import { type VegaWalletContextShape } from '@vegaprotocol/wallet';
+import { render, screen, waitFor } from '@testing-library/react';
+import {
+ VegaWalletContext,
+ type VegaWalletContextShape,
+} from '@vegaprotocol/wallet';
import { ReferralStatistics } from './referral-statistics';
import {
ReferralProgramDocument,
@@ -15,7 +18,7 @@ import {
StakeAvailableDocument,
type StakeAvailableQueryVariables,
type StakeAvailableQuery,
-} from './hooks/__generated__/StakeAvailable';
+} from '../../lib/hooks/__generated__/StakeAvailable';
import {
RefereesDocument,
type RefereesQueryVariables,
@@ -296,122 +299,99 @@ const refereesMock30: MockedResponse = {
},
};
-jest.mock('@vegaprotocol/wallet', () => {
- return {
- ...jest.requireActual('@vegaprotocol/wallet'),
- useVegaWallet: () => {
- const ctx: Partial = {
- pubKey: MOCK_PUBKEY,
- };
- return ctx;
- },
- };
-});
-
describe('ReferralStatistics', () => {
- it('displays apply code when no data has been found for given pubkey', () => {
- const { queryByTestId } = render(
+ const renderComponent = (mocks: MockedResponse[]) => {
+ const walletContext = {
+ pubKey: MOCK_PUBKEY,
+ isReadOnly: false,
+ sendTx: jest.fn(),
+ } as unknown as VegaWalletContextShape;
+
+ return render(
-
-
-
+
+
+
+
+
);
+ };
- expect(queryByTestId('referral-apply-code-form')).toBeInTheDocument();
+ it('displays apply code when no data has been found for given pubkey', () => {
+ renderComponent([]);
+ expect(
+ screen.queryByTestId('referral-apply-code-form')
+ ).toBeInTheDocument();
});
it('displays referrer stats when given pubkey is a referrer', async () => {
- const { queryByTestId } = render(
-
-
-
-
-
- );
+ renderComponent([
+ programMock,
+ referralSetAsReferrerMock,
+ noReferralSetAsRefereeMock,
+ stakeAvailableMock,
+ refereesMock,
+ refereesMock30,
+ ]);
await waitFor(() => {
expect(
- queryByTestId('referral-create-code-form')
+ screen.queryByTestId('referral-create-code-form')
).not.toBeInTheDocument();
- expect(queryByTestId('referral-statistics')).toBeInTheDocument();
- expect(queryByTestId('referral-statistics')?.dataset.as).toEqual(
+ expect(screen.queryByTestId('referral-statistics')).toBeInTheDocument();
+ expect(screen.queryByTestId('referral-statistics')?.dataset.as).toEqual(
'referrer'
);
// gets commision from 30 epochs query
- expect(queryByTestId('total-commission-value')).toHaveTextContent(
+ expect(screen.queryByTestId('total-commission-value')).toHaveTextContent(
'12,340'
);
});
});
it('displays referee stats when given pubkey is a referee', async () => {
- const { queryByTestId } = render(
-
-
-
-
-
- );
-
+ renderComponent([
+ programMock,
+ noReferralSetAsReferrerMock,
+ referralSetAsRefereeMock,
+ stakeAvailableMock,
+ refereesMock,
+ ]);
await waitFor(() => {
expect(
- queryByTestId('referral-create-code-form')
+ screen.queryByTestId('referral-create-code-form')
).not.toBeInTheDocument();
- expect(queryByTestId('referral-statistics')).toBeInTheDocument();
- expect(queryByTestId('referral-statistics')?.dataset.as).toEqual(
+ expect(screen.queryByTestId('referral-statistics')).toBeInTheDocument();
+ expect(screen.queryByTestId('referral-statistics')?.dataset.as).toEqual(
'referee'
);
});
});
it('displays eligibility warning when the set is no longer valid due to the referrers stake', async () => {
- const { queryByTestId } = render(
-
-
-
-
-
- );
+ renderComponent([
+ programMock,
+ noReferralSetAsReferrerMock,
+ referralSetAsRefereeMock,
+ nonEligibleStakeAvailableMock,
+ refereesMock,
+ ]);
await waitFor(() => {
expect(
- queryByTestId('referral-create-code-form')
+ screen.queryByTestId('referral-create-code-form')
).not.toBeInTheDocument();
- expect(queryByTestId('referral-statistics')).toBeInTheDocument();
- expect(queryByTestId('referral-statistics')?.dataset.as).toEqual(
+ expect(screen.queryByTestId('referral-statistics')).toBeInTheDocument();
+ expect(screen.queryByTestId('referral-statistics')?.dataset.as).toEqual(
'referee'
);
- expect(queryByTestId('referral-eligibility-warning')).toBeInTheDocument();
- expect(queryByTestId('referral-apply-code-form')).toBeInTheDocument();
+ expect(
+ screen.queryByTestId('referral-eligibility-warning')
+ ).toBeInTheDocument();
+ expect(
+ screen.queryByTestId('referral-apply-code-form')
+ ).toBeInTheDocument();
});
});
});
diff --git a/apps/trading/lib/hooks/use-create-referral-set.ts b/apps/trading/lib/hooks/use-create-referral-set.ts
index 51d62ad49..cefaa41e1 100644
--- a/apps/trading/lib/hooks/use-create-referral-set.ts
+++ b/apps/trading/lib/hooks/use-create-referral-set.ts
@@ -1,59 +1,28 @@
import {
- determineId,
- useVegaWallet,
+ useSimpleTransaction,
type CreateReferralSet,
+ type Options,
} from '@vegaprotocol/wallet';
-import { useState } from 'react';
import { useStakeAvailable } from './use-stake-available';
/**
* Manages state for creating a referral set or team
*/
-export const useCreateReferralSet = (opts?: {
- onSuccess?: (code: string) => void;
- onError?: (error: string) => void;
-}) => {
- const { pubKey, isReadOnly, sendTx } = useVegaWallet();
- const [err, setErr] = useState(null);
- const [code, setCode] = useState(null);
- const [status, setStatus] = useState<
- 'idle' | 'loading' | 'success' | 'error'
- >('idle');
-
+export const useCreateReferralSet = (opts?: Options) => {
const { stakeAvailable, requiredStake, isEligible } = useStakeAvailable();
+ const { status, result, error, send } = useSimpleTransaction({
+ onSuccess: opts?.onSuccess,
+ onError: opts?.onError,
+ });
+
const onSubmit = (tx: CreateReferralSet) => {
- if (isReadOnly || !pubKey) {
- setErr('Not connected');
- } else {
- setErr(null);
- setStatus('loading');
- setCode(null);
- sendTx(pubKey, tx)
- .then((res) => {
- if (!res) {
- throw new Error(`Invalid response: ${JSON.stringify(res)}`);
- }
- const code = determineId(res.signature);
- setCode(code);
- setStatus('success');
- opts?.onSuccess && opts.onSuccess(code);
- })
- .catch((err) => {
- if (err.message.includes('user rejected')) {
- setStatus('idle');
- return;
- }
- setStatus('error');
- setErr(err.message);
- opts?.onError && opts.onError(err.message);
- });
- }
+ send(tx);
};
return {
- err,
- code,
+ err: error ? error : null,
+ code: result ? result.id : null,
status,
stakeAvailable,
requiredStake,
diff --git a/libs/i18n/src/locales/en/trading.json b/libs/i18n/src/locales/en/trading.json
index ceb19ef09..e2428504f 100644
--- a/libs/i18n/src/locales/en/trading.json
+++ b/libs/i18n/src/locales/en/trading.json
@@ -7,6 +7,7 @@
"<0>No running Desktop App/CLI detected. Open your app now to connect or enter a0> <1>custom wallet location1>": "<0>No running Desktop App/CLI detected. Open your app now to connect or enter a0> <1>custom wallet location1>",
"A percentage of commission earned by the referrer": "A percentage of commission earned by the referrer",
"A successor to this market has been proposed": "A successor to this market has been proposed",
+ "As a team creator, you cannot switch teams": "As a team creator, you cannot switch teams",
"About the referral program": "About the referral program",
"Active": "Active",
"Activity Streak": "Activity Streak",
@@ -16,6 +17,7 @@
"Anonymous": "Anonymous",
"Anyone with the referral link can apply it to their key(s) of choice via an on chain transaction": "Anyone with the referral link can apply it to their key(s) of choice via an on chain transaction",
"Assessed over": "Assessed over",
+ "Are you sure you want to join team: {{team}}": "Are you sure you want to join team: {{team}}",
"Asset (1)": "Asset (1)",
"Assets": "Assets",
"Available to withdraw this epoch": "Available to withdraw this epoch",
@@ -27,6 +29,7 @@
"Best offer": "Best offer",
"Browse": "Browse",
"By using the Vega Console, you acknowledge that you have read and understood the <0>Vega Console Disclaimer0>": "By using the Vega Console, you acknowledge that you have read and understood the <0>Vega Console Disclaimer0>",
+ "Cancel": "Cancel",
"Change (24h)": "Change (24h)",
"Changes have been proposed for this market. <0>View proposals0>": "Changes have been proposed for this market. <0>View proposals0>",
"Chart": "Chart",
@@ -39,9 +42,12 @@
"Code must be be valid hex": "Code must be be valid hex",
"Collateral": "Collateral",
"Conduct your own due diligence and consult your financial advisor before making any investment decisions.": "Conduct your own due diligence and consult your financial advisor before making any investment decisions.",
+ "Confrim": "Confrim",
"Confirm in wallet...": "Confirm in wallet...",
+ "Confirming transaction...": "Confirming transaction...",
"Connect": "Connect",
"Connect wallet": "Connect wallet",
+ "Connect your wallet to join the team": "Connect your wallet to join the team",
"Connected node": "Connected node",
"Console": "Console",
"Continue sharing data": "Continue sharing data",
@@ -141,7 +147,7 @@
"Infrastructure": "Infrastructure",
"Interval: {{interval}}": "Interval: {{interval}}",
"Invite friends and earn rewards from the trading fees they pay. Stake those rewards to earn multipliers on future rewards.": "Invite friends and earn rewards from the trading fees they pay. Stake those rewards to earn multipliers on future rewards.",
- "Join this team": "Join this team",
+ "Join team": "Join team",
"Joined": "Joined",
"Joined at": "Joined at",
"Joined epoch": "Joined epoch",
@@ -213,6 +219,7 @@
"Order": "Order",
"Orderbook": "Orderbook",
"Orders": "Orders",
+ "Owner": "Owner",
"PRNT": "PRNT",
"Page not found": "Page not found",
"Parent of a market": "Parent of a market",
@@ -293,10 +300,15 @@
"Successors to this market have been proposed": "Successors to this market have been proposed",
"Supplied stake": "Supplied stake",
"Suspended due to price or liquidity monitoring trigger": "Suspended due to price or liquidity monitoring trigger",
+ "Switch team": "Switch team",
+ "Switching team will move you from '{{fromTeam}}' to '{{toTeam}}' at the end of the epoch. Are you sure?": "Switching team will move you from '{{fromTeam}}' to '{{toTeam}}' at the end of the epoch. Are you sure?",
"Target stake": "Target stake",
"Team": "Team",
"Team name": "Team name",
"Team creation transaction successful": "Team creation transaction successful",
+ "Team joined": "Team joined",
+ "Team switch successful. You will switch team at the end of the epoch.": "Team switch successful. You will switch team at the end of the epoch.",
+
"The amount of fees paid to liquidity providers across the whole market during the last epoch {{epoch}}.": "The amount of fees paid to liquidity providers across the whole market during the last epoch {{epoch}}.",
"The commission is taken from the infrastructure fee, maker fee, and liquidity provider fee, not from the referee": "The commission is taken from the infrastructure fee, maker fee, and liquidity provider fee, not from the referee",
"The external time weighted average price (TWAP) received from the data source defined in the data sourcing specification.": "The external time weighted average price (TWAP) received from the data source defined in the data sourcing specification.",
diff --git a/libs/i18n/src/locales/en/wallet.json b/libs/i18n/src/locales/en/wallet.json
index 88c80193e..756c4334b 100644
--- a/libs/i18n/src/locales/en/wallet.json
+++ b/libs/i18n/src/locales/en/wallet.json
@@ -48,6 +48,8 @@
"Supported browsers": "Supported browsers",
"The user rejected the wallet connection": "The user rejected the wallet connection",
"To complete your wallet connection, set your wallet network in your app to \"{{appChainId}}\".": "To complete your wallet connection, set your wallet network in your app to \"{{appChainId}}\".",
+ "Transaction could not be sent": "Transaction could not be sent",
+ "Transaction was not successful": "Transaction was not successful",
"Try again": "Try again",
"Understand the risk": "Understand the risk",
"Use the Desktop App/CLI": "Use the Desktop App/CLI",
@@ -57,6 +59,7 @@
"Verifying chain": "Verifying chain",
"View as party": "View as party",
"VIEW AS VEGA USER": "VIEW AS VEGA USER",
+ "Wallet rejected transaction": "Wallet rejected transaction",
"Wrong Network": "Wrong Network",
"Wrong network": "Wrong network",
"your browser": "your browser"
diff --git a/libs/wallet/src/SimpleTransaction.graphql b/libs/wallet/src/SimpleTransaction.graphql
new file mode 100644
index 000000000..4ac49f2af
--- /dev/null
+++ b/libs/wallet/src/SimpleTransaction.graphql
@@ -0,0 +1,17 @@
+fragment SimpleTransactionFields on TransactionResult {
+ partyId
+ hash
+ status
+ error
+}
+
+subscription SimpleTransaction($partyId: ID!) {
+ busEvents(partyId: $partyId, batchSize: 0, types: [TransactionResult]) {
+ type
+ event {
+ ... on TransactionResult {
+ ...SimpleTransactionFields
+ }
+ }
+ }
+}
diff --git a/libs/wallet/src/__generated__/SimpleTransaction.ts b/libs/wallet/src/__generated__/SimpleTransaction.ts
new file mode 100644
index 000000000..e7da4a563
--- /dev/null
+++ b/libs/wallet/src/__generated__/SimpleTransaction.ts
@@ -0,0 +1,57 @@
+import * as Types from '@vegaprotocol/types';
+
+import { gql } from '@apollo/client';
+import * as Apollo from '@apollo/client';
+const defaultOptions = {} as const;
+export type SimpleTransactionFieldsFragment = { __typename?: 'TransactionResult', partyId: string, hash: string, status: boolean, error?: string | null };
+
+export type SimpleTransactionSubscriptionVariables = Types.Exact<{
+ partyId: Types.Scalars['ID'];
+}>;
+
+
+export type SimpleTransactionSubscription = { __typename?: 'Subscription', busEvents?: Array<{ __typename?: 'BusEvent', type: Types.BusEventType, event: { __typename?: 'Deposit' } | { __typename?: 'TimeUpdate' } | { __typename?: 'TransactionResult', partyId: string, hash: string, status: boolean, error?: string | null } | { __typename?: 'Withdrawal' } }> | null };
+
+export const SimpleTransactionFieldsFragmentDoc = gql`
+ fragment SimpleTransactionFields on TransactionResult {
+ partyId
+ hash
+ status
+ error
+}
+ `;
+export const SimpleTransactionDocument = gql`
+ subscription SimpleTransaction($partyId: ID!) {
+ busEvents(partyId: $partyId, batchSize: 0, types: [TransactionResult]) {
+ type
+ event {
+ ... on TransactionResult {
+ ...SimpleTransactionFields
+ }
+ }
+ }
+}
+ ${SimpleTransactionFieldsFragmentDoc}`;
+
+/**
+ * __useSimpleTransactionSubscription__
+ *
+ * To run a query within a React component, call `useSimpleTransactionSubscription` and pass it any options that fit your needs.
+ * When your component renders, `useSimpleTransactionSubscription` returns an object from Apollo Client that contains loading, error, and data properties
+ * you can use to render your UI.
+ *
+ * @param baseOptions options that will be passed into the subscription, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
+ *
+ * @example
+ * const { data, loading, error } = useSimpleTransactionSubscription({
+ * variables: {
+ * partyId: // value for 'partyId'
+ * },
+ * });
+ */
+export function useSimpleTransactionSubscription(baseOptions: Apollo.SubscriptionHookOptions) {
+ const options = {...defaultOptions, ...baseOptions}
+ return Apollo.useSubscription(SimpleTransactionDocument, options);
+ }
+export type SimpleTransactionSubscriptionHookResult = ReturnType;
+export type SimpleTransactionSubscriptionResult = Apollo.SubscriptionResult;
\ No newline at end of file
diff --git a/libs/wallet/src/connectors/vega-connector.ts b/libs/wallet/src/connectors/vega-connector.ts
index b87c02e9a..1bc626005 100644
--- a/libs/wallet/src/connectors/vega-connector.ts
+++ b/libs/wallet/src/connectors/vega-connector.ts
@@ -437,6 +437,12 @@ export type ApplyReferralCode = {
};
};
+export type JoinTeam = {
+ joinTeam: {
+ id: string;
+ };
+};
+
export type CreateReferralSet = {
createReferralSet: {
isTeam: boolean;
@@ -465,6 +471,7 @@ export type Transaction =
| TransferBody
| LiquidityProvisionSubmission
| ApplyReferralCode
+ | JoinTeam
| CreateReferralSet;
export const isWithdrawTransaction = (
diff --git a/libs/wallet/src/index.ts b/libs/wallet/src/index.ts
index fc7c958cf..08aaa3c98 100644
--- a/libs/wallet/src/index.ts
+++ b/libs/wallet/src/index.ts
@@ -7,3 +7,9 @@ export * from './provider';
export * from './connect-dialog';
export * from './utils';
export * from './storage';
+export {
+ useSimpleTransaction,
+ type Status,
+ type Result,
+ type Options,
+} from './use-simple-transaction';
diff --git a/libs/wallet/src/use-simple-transaction.ts b/libs/wallet/src/use-simple-transaction.ts
new file mode 100644
index 000000000..a1e48d066
--- /dev/null
+++ b/libs/wallet/src/use-simple-transaction.ts
@@ -0,0 +1,114 @@
+import { useState } from 'react';
+import { useVegaWallet } from './use-vega-wallet';
+import { type Transaction } from './connectors';
+import {
+ useSimpleTransactionSubscription,
+ type SimpleTransactionFieldsFragment,
+} from './__generated__/SimpleTransaction';
+import { useT } from './use-t';
+import { determineId } from './utils';
+
+export type Status = 'idle' | 'requested' | 'pending' | 'confirmed';
+
+export type Result = {
+ txHash: string;
+ signature: string;
+ id: string;
+};
+
+export type Options = {
+ onSuccess?: (result: Result) => void;
+ onError?: (msg: string) => void;
+};
+
+export const useSimpleTransaction = (opts?: Options) => {
+ const t = useT();
+ const { pubKey, isReadOnly, sendTx } = useVegaWallet();
+
+ const [status, setStatus] = useState('idle');
+ const [result, setResult] = useState();
+ const [error, setError] = useState();
+
+ const send = async (tx: Transaction) => {
+ if (!pubKey) {
+ throw new Error('no pubKey');
+ }
+
+ if (isReadOnly) {
+ throw new Error('cant submit in read only mode');
+ }
+
+ setStatus('requested');
+
+ try {
+ const res = await sendTx(pubKey, tx);
+
+ if (!res) {
+ throw new Error(t('Transaction could not be sent'));
+ }
+
+ setStatus('pending');
+ setResult({
+ txHash: res?.transactionHash.toLowerCase(),
+ signature: res.signature,
+ id: determineId(res.signature),
+ });
+ } catch (err) {
+ if (err instanceof Error) {
+ if (err.message.includes('user rejected')) {
+ setStatus('idle');
+ } else {
+ setError(err.message);
+ opts?.onError?.(err.message);
+ }
+ } else {
+ const msg = t('Wallet rejected transaction');
+ setError(msg);
+ opts?.onError?.(msg);
+ }
+ }
+ };
+
+ useSimpleTransactionSubscription({
+ variables: { partyId: pubKey || '' },
+ skip: !pubKey || !result,
+ fetchPolicy: 'no-cache',
+ onData: ({ data }) => {
+ if (!result) {
+ throw new Error('simple transaction query started before result');
+ }
+
+ const e = data.data?.busEvents?.find((event) => {
+ if (
+ event.event.__typename === 'TransactionResult' &&
+ event.event.hash.toLowerCase() === result?.txHash
+ ) {
+ return true;
+ }
+
+ return false;
+ });
+
+ if (!e) return;
+
+ // Force type narrowing
+ const event = e.event as SimpleTransactionFieldsFragment;
+
+ if (event.status && !event.error) {
+ setStatus('confirmed');
+ opts?.onSuccess?.(result);
+ } else {
+ const msg = event?.error || t('Transaction was not successful');
+ setError(msg);
+ opts?.onError?.(msg);
+ }
+ },
+ });
+
+ return {
+ result,
+ error,
+ status,
+ send,
+ };
+};