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

react hooks #1687

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft

react hooks #1687

wants to merge 1 commit into from

Conversation

turbocrime
Copy link
Contributor

this contains some sketches for a new react package.

context-based, which there was some interest in moving away from. but

  • this is early wip, chill
  • context model facilitates scoping components to a specific provider, so a developer could use multiple providers

the usePenumbraQuery hook is the interesting part, as it wraps a PromiseClient with useQuery, so that callers may automatically consume penumbra requests as react queries. streams are consumed with Array.fromAsync so it is not suitable for long-streaming requests.

Copy link

changeset-bot bot commented Aug 13, 2024

⚠️ No Changeset found

Latest commit: 704552b

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copy link
Contributor

@VanishMax VanishMax left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now

If I were to write the examples for this package, I'd write smt like this:

Wrap the app in the Penumbra context provider

import { PenumbraContextProvider } from '@penumbra-zone/react';
import App from './app.tsx';

export const Main = () => {
	return (
		<React.StrictMode>
			<PenumbraContextProvider>
				<App />
			</PenumbraContextProvider>
		</React.StrictMode>
	);
};

const rootElement = document.getElementById('root') as HTMLDivElement;
createRoot(rootElement).render(<Main />);

Get the Penumbra connection state

import { usePenumbra } from '@penumbra-zone/react';

const Component = () => {
	const { connected, connect } = usePenumbra();

	if (!connected) {
		return (
			<button type="button" onClick={connect} />
		);
	}

	return (
		<button type="button" onClick={disconnect} />
	);
};

Request blockchain data by calling the queries

import { usePenumbraQuery } from '@penumbra-zone/react';
import { ViewService } from '@penumbra-zone/protobuf';
import { bech32mAddress } from '@penumbra-zone/bech32m/penumbra';

const Component = () => {
	const viewService = usePenumbraQuery(ViewService);
	const { data, error, isPending } = viewService.getAddressByIndex({ account: 0 });

	if (isPending) {
		return <p>loading...</p>;
	}

	return <p>You account address is {bech32mAddress(data)}</p>;
}

Or request blockchain data without wrappers

import { usePenumbraService } from '@penumbra-zone/react';
import { ViewService } from '@penumbra-zone/protobuf';

const viewService = usePenumbraService(ViewService);

const response = await viewService.getAddressByIndex({ account: 0 });

Proposal

Having the idea of the current implementation state, I think of more ways of using the React package:

  1. Firstly, we need hooks to represent static methods of PenumbraClient: getProviders, getProviderManifest, and the rest. It could be, for instance, the useProviders hook. It might wrap the provider list into the react query.
  2. Queries with simplified namings: useBalances, useAccount, etc. It will use the usePenumbraQuery under the hook but at least the names will be clear.
  3. Something to simplify the creation of a transaction. It might also be a naming issue but for me personally it is extremely hard to compose a transaction. This can also go into the client package, not only for react.

@turbocrime @grod220 wdyt?

console.log('providerConnected', providerConnected);

return (
<penumbraContext.Provider value={providerConnected ? penumbra : penumbra}>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should it be like this?

Suggested change
<penumbraContext.Provider value={providerConnected ? penumbra : penumbra}>
<penumbraContext.Provider value={penumbra}>

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably the value of the context should be a reactive object similar to PenumbraClient but without static methods, service, onConnectionStateChange and others non-needed functions replaced by hooks

@VanishMax VanishMax linked an issue Sep 2, 2024 that may be closed by this pull request
@turbocrime
Copy link
Contributor Author

turbocrime commented Sep 5, 2024

parts of minifront slices that might be helpful in developing hooks

  • sendTx

    sendTx: async () => {
    set(state => {
    state.send.txInProgress = true;
    });
    try {
    const req = assembleRequest(get().send);
    await planBuildBroadcast('send', req);
    set(state => {
    state.send.amount = '';
    });
    get().shared.balancesResponses.revalidate();
    } finally {
    set(state => {
    state.send.txInProgress = false;
    });
    }
    },

  • initiateSwapTx

    initiateSwapTx: async () => {
    set(state => {
    state.swap.instantSwap.txInProgress = true;
    });
    try {
    const swapReq = await assembleSwapRequest(get().swap);
    const swapTx = await planBuildBroadcast('swap', swapReq);
    const swapCommitment = getSwapCommitmentFromTx(swapTx);
    await issueSwapClaim(swapCommitment, swapReq.source);
    set(state => {
    state.swap.amount = '';
    });
    get().shared.balancesResponses.revalidate();
    } finally {
    set(state => {
    state.swap.instantSwap.txInProgress = false;
    });
    }
    },

  • delegate

    delegate: async () => {
    try {
    const stakingTokenMetadata = await getStakingTokenMetadata();
    const req = assembleDelegateRequest(get().staking, stakingTokenMetadata);
    // Reset form _after_ building the transaction planner request, since it depends on
    // the state.
    set(state => {
    state.staking.action = undefined;
    state.staking.validatorInfo = undefined;
    });
    await planBuildBroadcast('delegate', req);
    // Reload tokens to reflect their updated balances.
    void get().staking.loadDelegationsForCurrentAccount();
    get().shared.balancesResponses.revalidate();
    } finally {
    set(state => {
    state.staking.amount = '';
    });
    }
    },

  • undelegate

    undelegate: async () => {
    try {
    const req = assembleUndelegateRequest(get().staking);
    // Reset form _after_ assembling the transaction planner request, since it
    // depends on the state.
    set(state => {
    state.staking.action = undefined;
    state.staking.validatorInfo = undefined;
    });
    await planBuildBroadcast('undelegate', req);
    // Reload tokens to reflect their updated balances.
    void get().staking.loadDelegationsForCurrentAccount();
    get().shared.balancesResponses.revalidate();
    void get().staking.loadUnbondingTokensForCurrentAccount();
    } finally {
    set(state => {
    state.staking.amount = '';
    });
    }
    },

  • undelegateClaim

    undelegateClaim: async () => {
    const { account, unbondingTokensByAccount } = get().staking;
    const unbondingTokens = unbondingTokensByAccount.get(account)?.claimable.tokens;
    if (!unbondingTokens) {
    return;
    }
    try {
    const req = await assembleUndelegateClaimRequest({ account, unbondingTokens });
    if (!req) {
    return;
    }
    await planBuildBroadcast('undelegateClaim', req);
    // Reset form _after_ assembling the transaction planner request, since it
    // depends on the state.
    set(state => {
    state.staking.action = undefined;
    state.staking.validatorInfo = undefined;
    });
    // Reload tokens to reflect their updated balances.
    get().shared.balancesResponses.revalidate();
    void get().staking.loadUnbondingTokensForCurrentAccount();
    } finally {
    set(state => {
    state.staking.amount = '';
    });
    }
    },

  • ibc issueTx

    issueTx: async (getClient, address) => {
    const toast = new Toast();
    try {
    toast.loading().message('Issuing IBC transaction').render();
    if (!address) {
    throw new Error('Address not selected');
    }
    const { code, transactionHash, height } = await execute(get().ibcIn, address, getClient);
    // The transaction succeeded if and only if code is 0.
    if (code !== 0) {
    throw new Error(`Tendermint error: ${code}`);
    }
    // If we have a block explorer tx page link for this chain id, include it in toast
    const chainId = get().ibcIn.selectedChain?.chainId;
    const explorerTxPage = getExplorerPage(transactionHash, chainId);
    if (explorerTxPage) {
    toast.action(
    <a href={explorerTxPage} target='_blank' rel='noreferrer'>
    See details
    </a>,
    );
    }
    const chainName = get().ibcIn.selectedChain?.chainName;
    toast
    .success()
    .message(`IBC transaction succeeded! 🎉`)
    .description(
    `Transaction ${shorten(transactionHash, 8)} appeared on ${chainName} at height ${height}.`,
    )
    .render();
    } catch (e) {
    toast.error().message('Transaction error ❌').description(String(e)).render();
    }
    },

  • ibc getPlanRequest

    const getPlanRequest = async ({
    amount,
    selection,
    chain,
    destinationChainAddress,
    }: IbcOutSlice): Promise<TransactionPlannerRequest> => {
    if (!destinationChainAddress) {
    throw new Error('no destination chain address set');
    }
    if (!chain) {
    throw new Error('Chain not set');
    }
    if (!selection) {
    throw new Error('No asset selected');
    }
    const addressIndex = getAddressIndex(selection.accountAddress);
    const { address: returnAddress } = await penumbra
    .service(ViewService)
    .ephemeralAddress({ addressIndex });
    if (!returnAddress) {
    throw new Error('Error with generating IBC deposit address');
    }
    const { timeoutHeight, timeoutTime } = await getTimeout(chain.channelId);
    return new TransactionPlannerRequest({
    ics20Withdrawals: [
    {
    amount: toBaseUnit(
    BigNumber(amount),
    getDisplayDenomExponentFromValueView(selection.balanceView),
    ),
    denom: { denom: getMetadata(selection.balanceView).base },
    destinationChainAddress,
    returnAddress,
    timeoutHeight,
    timeoutTime,
    sourceChannel: chain.channelId,
    useCompatAddress: bech32ChainIds.includes(chain.chainId),
    },
    ],
    source: addressIndex,
    });
    };

there's plenty more, these are just some suggestions. anything in the minifront slices that assembles a plan request or a transaction request could contribute to a provided querier.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Renamed: Create React package for updated client package code
2 participants