Skip to content

Commit

Permalink
feat(explorer): transaction details page on /transaction/[requestKey] (
Browse files Browse the repository at this point in the history
…#2237)

* feat(explorer): transaction details page on /transaction/[requestKey]
* chore(explorer): disable @rushstack/no-new-null rule
* chore(explorer): allow JSX elements in DataRenderComponent
* feat(explorer): render execution and continuation differently
* feat(explorer): new features on transaction/[requestKey] page
- convert transaction/[requestKey] page to components
- add counterpart for crosschain transactions
- refactor query to a page specific one
* chore(explorer): move transaction-requestkey.graph.ts to src/graphql/pages
  • Loading branch information
alber70g authored Jun 6, 2024
1 parent f4a18fa commit 2a17292
Show file tree
Hide file tree
Showing 14 changed files with 509 additions and 206 deletions.
5 changes: 5 additions & 0 deletions .changeset/grumpy-cougars-retire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kadena/explorer": patch
---

added transaction/[requestKey] page
5 changes: 5 additions & 0 deletions packages/apps/explorer/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,9 @@ module.exports = {
extends: ['@kadena-dev/eslint-config/profile/next'],
ignorePatterns: ['**/__generated__/**'],
parserOptions: { tsconfigRootDir: __dirname },
rules: {
// graphql uses `null`s a lot, and we need to exclude them in types
// e.g. Exclude<TransactionResult, null> is a common pattern
'@rushstack/no-new-null': 'off',
},
};
2 changes: 1 addition & 1 deletion packages/apps/explorer/apollo.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ module.exports = {
localSchemaFile: __dirname + '/../graph/generated-schema.graphql',
},
includes: ['src/**/*.ts', 'src/**/*.tsx'],
excludes: ['**/node_modulues/**/*', 'src/__generated__/**/*'],
excludes: ['**/node_modules/**/*', 'src/__generated__/**/*'],
},
};
3 changes: 2 additions & 1 deletion packages/apps/explorer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"format:lint": "pnpm run lint:src --fix",
"format:md": "remark README.md -o --use @kadena-dev/markdown",
"format:src": "prettier . --cache --write",
"generate:sdk": "graphql-codegen --config codegen-sdk.yml",
"generate:sdk": "cache-sh -i \"{codegen-sdk.yml,./node_modules/@kadena/graph/generated-schema.graphql,src/**/*.graph.ts,src/__generated__/**}\" -- graphql-codegen --config codegen-sdk.yml",
"lint": "pnpm run /^lint:.*/",
"lint:fmt": "prettier . --cache --check",
"lint:pkg": "lint-package",
Expand Down Expand Up @@ -54,6 +54,7 @@
"@types/react-dom": "^18.2.25",
"@vanilla-extract/css": "1.14.2",
"@vanilla-extract/next-plugin": "2.4.0",
"cache-sh": "^1.2.1",
"concurrently": "^8.2.2",
"prettier": "~3.2.5",
"typescript": "5.4.5"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
interface IDataRenderComponentField {
type?: 'text' | 'code';
key: string;
value: string | string[];
value: string | string[] | JSX.Element | JSX.Element[];
link?: string;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
interface IDataRenderComponentField {
type?: 'text' | 'code';
key: string;
value: string | string[];
value: string | string[] | JSX.Element | JSX.Element[];
link?: string;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { headingClass, sectionClass } from './styles.css';
interface IDataRenderComponentField {
type?: 'text' | 'code';
key: string;
value: string | string[];
value: string | string[] | JSX.Element | JSX.Element[];
link?: string;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import type { TransactionRequestKeyQuery } from '@/__generated__/sdk';
import DataRenderComponent from '@/components/data-render-component/data-render-component';
import { ifNill } from '@/utils/ifNill';
import React from 'react';

type Transaction = Omit<
Exclude<TransactionRequestKeyQuery['transaction'], undefined | null>,
'result'
>;

export const TransactionRequestComponent: React.FC<{
transaction: Transaction;
}> = ({ transaction }) => {
return (
<>
{transaction.cmd.payload.__typename === 'ExecutionPayload' ? (
// Execution transaction

<DataRenderComponent
fields={[
{ key: 'Request Key (hash)', value: transaction.hash },
{
key: 'Env Data',
value: JSON.stringify(
JSON.parse(transaction.cmd.payload.data),
null,
2,
),
},
{
key: 'Code',
value: JSON.parse(transaction.cmd.payload.code ?? ''),
},
]}
/>
) : transaction.cmd.payload.__typename === 'ContinuationPayload' ? (
// Continuation

<DataRenderComponent
fields={[
{ key: 'Request Key (hash)', value: transaction.hash },
{
key: 'Data',
value: JSON.stringify(
JSON.parse(transaction.cmd.payload.data),
null,
2,
),
},
{
key: 'Pact ID',
value: ifNill(transaction.cmd.payload.pactId, ''),
},
{
key: 'Proof',
value: ifNill(transaction.cmd.payload.proof, ''),
},
{
key: 'Rollback',
value: JSON.stringify(transaction.cmd.payload.rollback),
},
{
key: 'Step',
value: JSON.stringify(transaction.cmd.payload.step),
},
]}
/>
) : null}

<DataRenderComponent
title="General"
fields={[
{ key: 'Chain', value: transaction.cmd.meta.chainId },
{ key: 'Created', value: transaction.cmd.meta.creationTime },
{ key: 'TTL', value: transaction.cmd.meta.ttl },
{
key: 'Sender',
value: transaction.cmd.meta.sender,
link: `/account/${transaction.cmd.meta.sender}`,
},
{ key: 'Gas Limit', value: transaction.cmd.meta.gasLimit },
{ key: 'Gas Price', value: transaction.cmd.meta.gasPrice },
{ key: 'Nonce', value: transaction.cmd.nonce },
]}
/>

<DataRenderComponent
title="Signers"
fields={transaction.cmd.signers
// .sort((a, b) => a.orderIndex - b.orderIndex)
.map((signer) => [
{
key: 'Public key',
value: signer.pubkey,
},
{ key: 'Scheme', value: signer.scheme ?? '' },
{
key: 'Capabilities',
value: (
<DataRenderComponent
fields={signer.clist.map((capability) => ({
key: capability.name,
value: (
<>
{(JSON.parse(capability.args) as string[])
.map((n, i) => <p key={i}>{JSON.stringify(n)}</p>)
.flat()}
</>
),
}))}
/>
),
},
])
.flat()}
/>

<DataRenderComponent
title="Signatures"
fields={transaction.sigs.map((s) => ({ key: '', value: s.sig }))}
/>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import type {
TransactionMempoolInfo,
TransactionRequestKeyQuery,
Transfer,
} from '@/__generated__/sdk';
import DataRenderComponent from '@/components/data-render-component/data-render-component';
import React, { useEffect } from 'react';
import { ifNill } from '../../utils/ifNill';

type TransactionResult = Exclude<
TransactionRequestKeyQuery['transaction'],
undefined | null
>['result'];

export const TransactionResultComponent: React.FC<{
transactionResult: TransactionResult;
}> = ({ transactionResult }) => {
const [crosschainTransfer, setCrosschainTransfer] =
React.useState<Transfer>();

useEffect(() => {
if (
transactionResult &&
transactionResult.__typename === 'TransactionResult'
) {
const xchainTx = getCrosschainTransfer(transactionResult);
setCrosschainTransfer(xchainTx);
}
}, [transactionResult]);

if (transactionResult.__typename !== 'TransactionResult') {
return null;
}

return (
<>
<DataRenderComponent
title="Block"
fields={[
{ key: 'Height', value: transactionResult.block.height },
{ key: 'Hash', value: transactionResult.block.hash },
{ key: 'Created', value: transactionResult.block.creationTime },
]}
/>

{transactionResult.__typename === 'TransactionResult' &&
crosschainTransfer && (
<DataRenderComponent
title="Crosschain Transfer"
fields={[
{
key: 'Type',
// note: this might be confusing
// where the COUNTERPART SENDER is empty, the COUNTERPART is
// the minting transaction (finisher transaction)
// That makes the CURRENT transaction the sender
value:
getCrosschainTransfer(
transactionResult,
).senderAccount.trim() === ''
? 'Initiation transaction'
: 'Finisher transaction',
},
{
key: 'Counterpart',
value: getCrosschainTransfer(transactionResult).requestKey,
link: `/transaction/${getCrosschainTransfer(transactionResult).requestKey}`,
},
]}
/>
)}

<DataRenderComponent
title="Result"
fields={[
{
key: 'Result',
value:
transactionResult.badResult !== null
? `Failed: ${ifNill(transactionResult.badResult, '')}`
: `Success: ${ifNill(transactionResult.goodResult, '')}`,
},
{ key: 'Logs', value: transactionResult.logs },
{ key: 'Gas', value: transactionResult.gas },
{ key: 'Transaction ID', value: transactionResult.transactionId },
{
key: 'Continuation',
value: (
<DataRenderComponent
fields={objectToDataRenderComponentFields({
...JSON.parse(transactionResult.continuation ?? ''),
})}
/>
),
},
{ key: 'Metadata', value: transactionResult.metadata },
]}
/>

<DataRenderComponent
title="Events"
fields={
transactionResult.events.edges
.map(
(edge) =>
edge && {
key: edge.node.qualifiedName,
value: mapParameters(edge.node.parameters),
},
)
.filter(Boolean) as { key: string; value: string }[]
}
/>
</>
);
};

function mapParameters(
parameters: string | null | undefined,
): string | React.JSX.Element {
if (!parameters) return '';
try {
return (
<>
{(JSON.parse(parameters) as unknown[]).map((param) => {
if (typeof param === 'object') {
// eslint-disable-next-line react/jsx-key
return <p>{JSON.stringify(param)}</p>;
}
if (typeof param === 'string') {
// eslint-disable-next-line react/jsx-key
return <p>{param}</p>;
}
// eslint-disable-next-line react/jsx-key
return <p>{JSON.stringify(param)}</p>;
})}
</>
);
} catch {
return parameters;
}
}

function objectToDataRenderComponentFields(
obj: Record<string, unknown>,
): { key: string; value: string }[] {
return Object.entries(obj).map(([key, value]) => ({
key,
value: JSON.stringify(value),
}));
}

type TransactionMempoolOrResult = Exclude<
TransactionRequestKeyQuery['transaction'],
undefined | null
>['result'];

type TxResult = Exclude<TransactionMempoolOrResult, TransactionMempoolInfo>;

function getCrosschainTransfer(transaction: TxResult): Transfer {
return transaction.transfers.edges.find(
(edge) => edge?.node.crossChainTransfer !== null,
)?.node.crossChainTransfer as Transfer;
}
Loading

0 comments on commit 2a17292

Please sign in to comment.