Skip to content

Commit

Permalink
Merge pull request #30 from spacemeshos/feat-vesting-vault
Browse files Browse the repository at this point in the history
Add support of Spawn txs + Vesting and Vault accounts interaction
  • Loading branch information
brusherru authored Jul 11, 2024
2 parents 7fd1131 + 56d7c2a commit 0914cfe
Show file tree
Hide file tree
Showing 21 changed files with 698 additions and 337 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"@noble/ed25519": "^2.1.0",
"@scure/bip39": "^1.2.2",
"@spacemesh/ed25519-bip32": "^0.2.1",
"@spacemesh/sm-codec": "^0.7.1",
"@spacemesh/sm-codec": "^0.7.2",
"@tabler/icons-react": "^3.1.0",
"@tanstack/react-virtual": "^3.3.0",
"@uidotdev/usehooks": "^2.4.1",
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/txUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ describe('collectTxIdsByAddress', () => {
principal,
template: {
name: TemplateName.SingleSig,
methodName: MethodName.SelfSpawn,
methodName: MethodName.Spawn,
},
} as Transaction);
const spend = (id: string, principal: string, destination: string) =>
Expand Down
31 changes: 17 additions & 14 deletions src/api/requests/tx.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import {
SpawnTransaction,
SpendTransaction,
StdPublicKeys,
StdTemplates,
} from '@spacemesh/sm-codec';
import { SpawnTransaction, SpendTransaction } from '@spacemesh/sm-codec';

import { Bech32Address } from '../../types/common';
import { Transaction } from '../../types/tx';
import { fromBase64, toBase64 } from '../../utils/base64';
import { getWords } from '../../utils/bech32';
import { toHexString } from '../../utils/hexString';
import { getMethodName, getTemplateNameByAddress } from '../../utils/templates';
import {
getMethodName,
getTemplateMethod,
getTemplateNameByAddress,
} from '../../utils/templates';
import { parseResponse } from '../schemas/error';
import {
EstimateGasResponseSchema,
Expand All @@ -18,8 +18,7 @@ import {
TransactionResponseSchema,
TransactionResultStatus,
TransactionState,
WithLayer,
WithState,
WithExtraData,
} from '../schemas/tx';

import getFetchAll from './getFetchAll';
Expand Down Expand Up @@ -52,7 +51,7 @@ export const fetchTransactionsChunk = async (
address: Bech32Address,
limit = 100,
offset = 0
): Promise<(TransactionResponseObject & WithLayer & WithState)[]> =>
): Promise<(TransactionResponseObject & WithExtraData)[]> =>
fetch(`${rpc}/spacemesh.v2alpha1.TransactionService/List`, {
method: 'POST',
body: JSON.stringify({
Expand All @@ -69,8 +68,12 @@ export const fetchTransactionsChunk = async (
transactions.map((tx) => ({
...tx.tx,
layer: tx.txResult?.layer || 0,
state: getTxState(tx.txResult?.status, tx.txState),
state: getTxState(
tx.txResult?.status,
tx.txState || 'TRANSACTION_STATE_UNSPECIFIED'
),
message: tx.txResult?.message,
touched: tx.txResult?.touchedAddresses || [tx.tx.principal],
}))
);

Expand All @@ -83,9 +86,8 @@ export const fetchTransactionsByAddress = async (
const txs = await fetchTransactions(rpc, address);

return txs.map((tx) => {
// TODO: Support other templates
const template =
StdTemplates[StdPublicKeys.SingleSig].methods[tx.method as 0 | 16];
const templateAddress = toHexString(getWords(tx.template));
const template = getTemplateMethod(templateAddress, tx.method);
try {
const parsedRaw = template.decode(fromBase64(tx.raw));
const parsed =
Expand Down Expand Up @@ -113,6 +115,7 @@ export const fetchTransactionsByAddress = async (
parsed: parsed.Payload.Arguments,
state: tx.state,
message: tx.message,
touched: tx.touched,
} satisfies Transaction<(typeof parsed)['Payload']['Arguments']>;
} catch (err) {
/* eslint-disable no-console */
Expand Down
10 changes: 9 additions & 1 deletion src/api/schemas/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ export const parseResponse =
try {
return schema.parse(input);
} catch (err) {
throw toError(ErrorResponse.parse(input));
if (
typeof input === 'object' &&
!!input &&
Object.hasOwn(input, 'message')
) {
throw toError(ErrorResponse.parse(input));
} else {
throw err;
}
}
};
15 changes: 10 additions & 5 deletions src/api/schemas/tx.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { z } from 'zod';

import { Bech32Address } from '../../types/common';

import { Bech32AddressSchema } from './address';
import { Base64Schema } from './common';
import { BigIntStringSchema } from './strNumber';
Expand Down Expand Up @@ -60,8 +62,8 @@ export const TransactionStateEnumSchema = z.enum([

export const TransactionResponseObjectSchema = z.object({
tx: TransactionSchema,
txResult: z.optional(TransactionResultSchema),
txState: z.optional(TransactionStateEnumSchema),
txResult: z.nullable(TransactionResultSchema),
txState: z.nullable(TransactionStateEnumSchema),
});

// Responses
Expand All @@ -80,9 +82,12 @@ export type TransactionResultStatus = z.infer<
typeof TransactionResultStatusSchema
>;

export type WithLayer = { layer: number };

export type WithState = { state: TransactionState; message?: string };
export type WithExtraData = {
layer: number;
state: TransactionState;
touched?: Bech32Address[];
message?: string;
};

export const EstimateGasResponseSchema = z.object({
recommendedMaxGas: BigIntStringSchema,
Expand Down
2 changes: 2 additions & 0 deletions src/components/CreateAccountModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ function CreateAccountModal({
control,
handleSubmit,
setValue,
getValues,
formState: { errors, isSubmitted },
} = useForm<FormValues>({
resolver: zodResolver(FormSchema),
Expand Down Expand Up @@ -178,6 +179,7 @@ function CreateAccountModal({
isSubmitted={isSubmitted}
isRequired
setValue={setValue}
getValues={getValues}
/>
<FormInput
label="Total amount"
Expand Down
31 changes: 27 additions & 4 deletions src/components/FormAddressSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import { PropsWithChildren, ReactNode, useEffect, useState } from 'react';
import {
PropsWithChildren,
ReactNode,
useEffect,
useRef,
useState,
} from 'react';
import {
FieldError,
FieldErrors,
FieldValues,
get,
Path,
PathValue,
UseFormGetValues,
UseFormRegister,
UseFormSetValue,
UseFormUnregister,
Expand Down Expand Up @@ -39,6 +46,7 @@ type Props<
register: UseFormRegister<T>;
unregister: UseFormUnregister<T>;
setValue: UseFormSetValue<T>;
getValues: UseFormGetValues<T>;
errors: FieldErrors<T>;
isSubmitted?: boolean;
isRequired?: boolean;
Expand All @@ -56,17 +64,26 @@ function FormAddressSelect<T extends FieldValues, FieldName extends Path<T>>({
children = '',
defaultForeign = false,
setValue,
getValues,
}: Props<T, FieldName>): JSX.Element {
const [origin, setOrigin] = useState(
defaultForeign ? Origin.Foreign : Origin.Local
);
const prevOrigin = useRef(origin);
useEffect(() => () => unregister(fieldName), [unregister, fieldName, origin]);
useEffect(() => {
const firstLocal = accounts?.[0]?.address;
if (origin === Origin.Local && !!firstLocal) {
const val = getValues(fieldName);
const foundVal = accounts.find((x) => x.address === val);
if (
prevOrigin.current !== origin &&
origin === Origin.Local &&
!!firstLocal &&
!foundVal
) {
setValue(fieldName, firstLocal as PathValue<T, FieldName>);
}
}, [origin, accounts, setValue, fieldName]);
}, [prevOrigin, origin, accounts, setValue, fieldName, getValues]);

const error = get(errors, fieldName) as FieldError | undefined;
const renderInputs = () => {
Expand All @@ -76,6 +93,9 @@ function FormAddressSelect<T extends FieldValues, FieldName extends Path<T>>({
<Input
{...register(fieldName, {
required: isRequired ? 'Please pick the address' : false,
value: (!defaultForeign
? accounts[0]?.address ?? ''
: '') as PathValue<T, FieldName>,
validate: (val) => {
try {
Bech32AddressSchema.parse(val);
Expand Down Expand Up @@ -108,7 +128,10 @@ function FormAddressSelect<T extends FieldValues, FieldName extends Path<T>>({
<>
<RadioGroup
size="sm"
onChange={(next) => setOrigin(next as Origin)}
onChange={(next) => {
prevOrigin.current = origin;
setOrigin(next as Origin);
}}
value={origin}
mb={1}
>
Expand Down
67 changes: 65 additions & 2 deletions src/components/TxDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
import { useCopyToClipboard } from '@uidotdev/usehooks';

import { Transaction } from '../types/tx';
import { generateAddress } from '../utils/bech32';
import { formatTimestamp } from '../utils/datetime';
import { ExplorerDataType } from '../utils/getExplorerUrl';
import { toHexString } from '../utils/hexString';
Expand All @@ -29,8 +30,12 @@ import {
formatTxState,
getDestinationAddress,
getStatusColor,
isDrainTransaction,
isMultiSigSpawnTransaction,
isSingleSigSpawnTransaction,
isSpendTransaction,
isVaultSpawnTransaction,
isVestingSpawnTransaction,
} from '../utils/tx';

import ExplorerButton from './ExplorerButton';
Expand Down Expand Up @@ -91,7 +96,7 @@ function Row({ label, value, isCopyable, explorer }: RowProps) {
);
}

const renderTxSpecificData = (tx: Transaction) => {
const renderTxSpecificData = (hrp: string, tx: Transaction) => {
if (isSpendTransaction(tx)) {
const destination = getDestinationAddress(tx, tx.principal);
if (!destination) {
Expand All @@ -118,6 +123,62 @@ const renderTxSpecificData = (tx: Transaction) => {
/>
);
}
if (isMultiSigSpawnTransaction(tx) || isVestingSpawnTransaction(tx)) {
return (
<>
<Row
label="Required signatures"
value={String(tx.parsed.Required)}
isCopyable
/>
<Row
label="Public Keys"
value={tx.parsed.PublicKeys.map((x) => toHexString(x, true)).join(
', '
)}
isCopyable
/>
</>
);
}
if (isVaultSpawnTransaction(tx)) {
return (
<>
<Row
label="Owner"
value={generateAddress(tx.parsed.Owner, hrp)}
isCopyable
/>
<Row label="Total Amount" value={formatSmidge(tx.parsed.TotalAmount)} />
<Row
label="Initial Unlocked Amount"
value={formatSmidge(tx.parsed.InitialUnlockAmount)}
/>
<Row
label="Vesting Start (Layer)"
value={String(tx.parsed.VestingStart)}
/>
<Row label="Vesting End (Layer)" value={String(tx.parsed.VestingEnd)} />
</>
);
}
if (isDrainTransaction(tx)) {
return (
<>
<Row
label="Vault"
value={generateAddress(tx.parsed.Vault, hrp)}
isCopyable
/>
<Row label="Amount" value={formatSmidge(tx.parsed.Amount)} />
<Row
label="Destination"
value={generateAddress(tx.parsed.Destination, hrp)}
isCopyable
/>
</>
);
}

return null;
};
Expand All @@ -129,6 +190,7 @@ type TxDetailsProps = {
genesisTime: number;
layerDurationSec: number;
layersPerEpoch: number;
hrp: string;
};

function TxDetails({
Expand All @@ -138,6 +200,7 @@ function TxDetails({
genesisTime,
layerDurationSec,
layersPerEpoch,
hrp,
}: TxDetailsProps): JSX.Element | null {
return (
<Portal>
Expand Down Expand Up @@ -196,7 +259,7 @@ function TxDetails({
label="Transaction type"
value={`${tx.template.name}.${tx.template.methodName}`}
/>
{renderTxSpecificData(tx)}
{renderTxSpecificData(hrp, tx)}
</Box>
<Flex mt={6}>
<Box flex={1} pr={4}>
Expand Down
1 change: 0 additions & 1 deletion src/components/TxList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ function TxList({
getScrollElement: () => parentRef.current,
estimateSize: () => 82.5,
});

return (
<TabPanel ref={parentRef} flexGrow={1} height={1} overflow="auto">
<div
Expand Down
6 changes: 3 additions & 3 deletions src/components/sendTx/ConfirmationModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ type ConfirmationModalProps = ConfirmationData & {
const renderTemplateSpecificFields = (form: FormValues) => {
switch (form.templateAddress) {
case StdPublicKeys.SingleSig: {
if (form.payload.methodSelector === MethodSelectors.SelfSpawn) {
if (form.payload.methodSelector === MethodSelectors.Spawn) {
const args = SingleSigSpawnSchema.parse(form.payload);
return <PreviewDataRow label="Public key" value={args.PublicKey} />;
}
Expand All @@ -99,7 +99,7 @@ const renderTemplateSpecificFields = (form: FormValues) => {
}
case StdPublicKeys.MultiSig:
case StdPublicKeys.Vesting: {
if (form.payload.methodSelector === MethodSelectors.SelfSpawn) {
if (form.payload.methodSelector === MethodSelectors.Spawn) {
const args = MultiSigSpawnSchema.parse(form.payload);
return (
<>
Expand Down Expand Up @@ -149,7 +149,7 @@ const renderTemplateSpecificFields = (form: FormValues) => {
);
}
case StdPublicKeys.Vault: {
if (form.payload.methodSelector === MethodSelectors.SelfSpawn) {
if (form.payload.methodSelector === MethodSelectors.Spawn) {
const args = VaultSpawnSchema.parse(form.payload);
return (
<>
Expand Down
Loading

0 comments on commit 0914cfe

Please sign in to comment.