Skip to content
This repository has been archived by the owner on Aug 16, 2024. It is now read-only.

Commit

Permalink
react-components: Migrate 'useContractSchemaRpc' from contractupdate …
Browse files Browse the repository at this point in the history
…sample (#46)

The sample `contractupdate` had some nice utilities for resolving the
on-chain schema of a smart contract. It handles the greasy details of
what wasm sections to check (in which order) for the different module
versions. This is error prone and not something a dApp developer would
want to mess with themselves.

This PR moves moves these utils into a hook `useModuleSchemaRpc` as
part of `react-components`. As we don't use `Result` in that package,
the functions have been rewritten to use exceptions instead.
  • Loading branch information
bisgardo authored Apr 17, 2024
1 parent 3f6885a commit a045f3b
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 102 deletions.
6 changes: 6 additions & 0 deletions packages/react-components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- Hook `useModuleSchemaRpc` for fetching the schema of a smart contract module from the chain.

## [0.5.1] - 2024-03-22

- Dependency on `@concordium/wallet-connectors` bumped to v0.5.1+.
Expand Down
30 changes: 29 additions & 1 deletion packages/react-components/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ _Example: Look up the info of a smart contract by its index specified in an inpu

```typescript jsx
import React, { useState } from 'react';
import { Network, useContractSelector, WalletConnection } from '@concordium/react-components';
import { Network, useContractSelector } from '@concordium/react-components';
import { ConcordiumGRPCClient } from '@concordium/web-sdk';

interface Props {
Expand All @@ -143,6 +143,34 @@ export function ContractStuff({ rpc }: Props) {
Use the hook [`useGrpcClient`](#usegrpcclient) below to obtain a `ConcordiumGRPCClient` instance.
See [the sample dApp](../../samples/contractupdate/src/Root.tsx) for a complete example.

### [`useModuleSchemaRpc`](./src/useModuleSchemaRpc.ts)

Hook for resolving the schema of a smart contract from the chain.
The schema is used to construct the payload of invocations of the smart contract.

_Example: Fetch schema of a provided smart contract_

```typescript jsx
import React, { useState } from 'react';
import { Info, Network, Schema, useModuleSchemaRpc } from '@concordium/react-components';
import { ConcordiumGRPCClient } from '@concordium/web-sdk';

interface Props {
rpc: ConcordiumGRPCClient;
contract: Info;
}

export function ContractSchemaStuff({ rpc }: Props) {
const [schemaRpcError, setSchemaRpcError] = useState('');
const schemaRpcResult = useModuleSchemaRpc(rpc, contract.moduleRef, setSchemaRpcError);
const schema: Schema = schemaRpcResult?.schema;
// ...
}
```

Use the hook [`useGrpcClient`](#usegrpcclient) below to obtain a `ConcordiumGRPCClient` instance.
See [the sample dApp](../../samples/contractupdate/src/Root.tsx) for a complete example.

### [`useGrpcClient`](./src/useGrpcClient.ts)

React hook that obtains a gRPC Web client for interacting with a node on the appropriate network.
Expand Down
1 change: 1 addition & 0 deletions packages/react-components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './useConnect';
export * from './useConnection';
export * from './useContractSelector';
export * from './useGrpcClient';
export * from './useModuleSchemaRpc';
export * from './useWalletConnectorSelector';
export * from './WithWalletConnector';
export * from '@concordium/wallet-connectors';
2 changes: 1 addition & 1 deletion packages/react-components/src/useContractSelector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export interface ContractSelector {

/**
* React hook to look up a smart contract's data and state from its index.
* @param rpc gRPC client through which to perform the lookup. The JSON-RPC client is supported for backwards compatibility only.
* @param rpc gRPC client through which to perform the lookup.
* @param input The index of the contract to look up.
* @return The resolved contract and related state.
*/
Expand Down
98 changes: 98 additions & 0 deletions packages/react-components/src/useModuleSchemaRpc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { Buffer } from 'buffer/';
import { useEffect, useState } from 'react';
import { Schema, moduleSchema } from '@concordium/wallet-connectors';
import { ConcordiumGRPCClient, ModuleReference, SchemaVersion } from '@concordium/web-sdk';

/**
* The result of extracting a smart contract {@link Schema} from a WebAssembly module.
*/
export interface SchemaResult {
/**
* The name of the custom section in which the schema was found.
*/
sectionName: string;
/**
* The resolved schema.
*/
schema: Schema;
}

function findCustomSections(m: WebAssembly.Module, moduleVersion: number) {
function getCustomSections(sectionName: string, schemaVersion: SchemaVersion | undefined) {
const s = WebAssembly.Module.customSections(m, sectionName);
return s.length === 0 ? undefined : { sectionName, schemaVersion, contents: s };
}

// First look for section containing schema with embedded version, then "-v1" or "-v2" depending on the module version.
// See also comment in 'useModuleSchemaRpc'.
switch (moduleVersion) {
case 0:
return (
getCustomSections('concordium-schema', undefined) || // always v0
getCustomSections('concordium-schema-v1', SchemaVersion.V0) // v0 (not a typo)
);
case 1:
return (
getCustomSections('concordium-schema', undefined) || // v1, v2, or v3
getCustomSections('concordium-schema-v2', SchemaVersion.V1) // v1 (not a typo)
);
}
return getCustomSections('concordium-schema', undefined); // expecting to find this section in future module versions
}

function findSchema(m: WebAssembly.Module, moduleVersion: number): SchemaResult | undefined {
const sections = findCustomSections(m, moduleVersion);
if (!sections) {
return undefined;
}
const { sectionName, schemaVersion, contents } = sections;
if (contents.length !== 1) {
throw new Error(`unexpected size of custom section "${sectionName}"`);
}
return { sectionName, schema: moduleSchema(Buffer.from(contents[0]), schemaVersion) };
}

async function fetchSchema(rpc: ConcordiumGRPCClient, moduleRef: string) {
const { version, source } = await rpc.getModuleSource(ModuleReference.fromHexString(moduleRef));
if (source.length === 0) {
throw new Error('module source is empty');
}
// The module can contain a schema in one of two different custom sections.
// The supported sections depend on the module version.
// The schema version can be either defined by the section name or embedded into the actual schema:
// - Both v0 and v1 modules support the section 'concordium-schema' where the schema includes the version.
// - For v0 modules this is always a v0 schema.
// - For v1 modules this can be a v1, v2, or v3 schema.
// - V0 modules additionally support section 'concordium-schema-v1' which always contain a v0 schema (not a typo).
// - V1 modules additionally support section 'concordium-schema-v2' which always contain a v1 schema (not a typo).
// The section 'concordium-schema' is the most common and is what the current tooling produces.
const module = await WebAssembly.compile(source);
return findSchema(module, version);
}

/**
* Hook for resolving the {@link Schema} of a smart contract module from the chain.
* The schema may be used to construct the payload of invocations of smart contracts that are instances of this module.
* @param rpc gRPC client through which to perform the lookup.
* @param moduleRef The reference of the module for which to lookup.
* @param setError Function that is invoked with any error that occurred while resolving the schema (e.g. module was not found or it was malformed).
* @return The schema wrapped into a {@link SchemaResult} or undefined if no schema was found.
*/
export function useModuleSchemaRpc(
rpc: ConcordiumGRPCClient,
moduleRef: string,
setError: (err: string) => void
): SchemaResult | undefined {
const [result, setResult] = useState<SchemaResult | undefined>();
useEffect(() => {
fetchSchema(rpc, moduleRef)
.then((r) => {
setResult(r);
setError('');
})
.catch((err) => {
setError(err);
});
}, [rpc, moduleRef]);
return result;
}
57 changes: 31 additions & 26 deletions samples/contractupdate/src/ContractInvoker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
moduleSchemaFromBase64,
typeSchemaFromBase64,
} from '@concordium/react-components';
import { useModuleSchemaRpc } from '@concordium/react-components';
import {
AccountAddress,
AccountTransactionType,
Expand All @@ -19,7 +20,6 @@ import {
ReceiveName,
SchemaVersion,
} from '@concordium/web-sdk';
import { useContractSchemaRpc } from './useContractSchemaRpc';
import { errorString } from './util';

interface ContractParamEntry {
Expand Down Expand Up @@ -99,6 +99,17 @@ function ccdScanUrl(network: Network, txHash: string | undefined) {
return `${network.ccdScanBaseUrl}/?dcount=1&dentity=transaction&dhash=${txHash}`;
}

type SchemaResult = SchemaFromInput | SchemaFromRpc;
interface SchemaFromInput {
fromRpc: false;
schema: Schema | undefined;
}
interface SchemaFromRpc {
fromRpc: true;
schema: Schema;
sectionName: string;
}

export function ContractInvoker({ rpc, network, connection, connectedAccount, contract }: ContractInvokerProps) {
const [selectedMethodIndex, setSelectedMethodIndex] = useState(0);
const [schemaInput, setSchemaInput] = useState('');
Expand All @@ -108,7 +119,8 @@ export function ContractInvoker({ rpc, network, connection, connectedAccount, co
setSchemaInput('');
}, [contract]);

const schemaRpcResult = useContractSchemaRpc(rpc, contract);
const [schemaRpcError, setSchemaRpcError] = useState('');
const schemaRpcResult = useModuleSchemaRpc(rpc, contract.moduleRef, setSchemaRpcError);
const [schemaTypeInput, setSchemaTypeInput] = useState(DEFAULT_SCHEMA_TYPE);

const [contractParams, setContractParams] = useState<Array<ContractParamEntry>>([]);
Expand All @@ -123,7 +135,7 @@ export function ContractInvoker({ rpc, network, connection, connectedAccount, co
() => ok(Object.fromEntries(contractParams.map((p) => [p.name, parseParamValue(p.value)]))),
[contractParams]
);
const schemaResult = useMemo(() => {
const schemaResult: Result<SchemaResult, string> = useMemo(() => {
let input = schemaInput.trim();
if (input) {
try {
Expand All @@ -132,10 +144,10 @@ export function ContractInvoker({ rpc, network, connection, connectedAccount, co
return err('schema is not valid base64');
}
}
return (
schemaRpcResult?.map((r) => ({ fromRpc: true, schema: r?.schema })) ||
ok({ fromRpc: false, schema: undefined })
);
if (schemaRpcResult) {
return ok({ ...schemaRpcResult, fromRpc: true });
}
return ok({ fromRpc: false, schema: undefined });
}, [schemaInput, schemaTypeInput, schemaRpcResult]);
const amountResult = useMemo(() => {
try {
Expand Down Expand Up @@ -180,13 +192,10 @@ export function ContractInvoker({ rpc, network, connection, connectedAccount, co
return (
<>
<h4>Update Contract</h4>
{schemaRpcResult?.match(
() => undefined,
(e) => (
<Alert variant="warning">
Error fetching contract schema from chain: <code>{e}</code>.
</Alert>
)
{schemaRpcError && (
<Alert variant="warning">
Error fetching contract schema from chain: <code>{schemaRpcError}</code>.
</Alert>
)}
<Form>
<Form.Group as={Row} className="mb-3">
Expand Down Expand Up @@ -215,8 +224,8 @@ export function ContractInvoker({ rpc, network, connection, connectedAccount, co
() => false
)}
isInvalid={schemaResult.isErr()}
placeholder={schemaRpcResult?.match(
(v) => v && v.schema.value.toString('base64'),
placeholder={schemaResult.match(
({ fromRpc, schema }) => (fromRpc ? schema.value.toString('base64') : undefined),
() => undefined
)}
/>
Expand Down Expand Up @@ -250,16 +259,12 @@ export function ContractInvoker({ rpc, network, connection, connectedAccount, co
</Dropdown.Item>
</DropdownButton>
{schemaResult.match(
() =>
schemaRpcResult?.match(
(v) =>
v && (
<Form.Control.Feedback>
Using schema from section <code>{v.sectionName}</code> of the
contract's module.
</Form.Control.Feedback>
),
() => undefined
(r) =>
r.fromRpc && (
<Form.Control.Feedback>
Using schema from section <code>{r.sectionName}</code> of the contract's
module.
</Form.Control.Feedback>
),
(e) => (
<Form.Control.Feedback type="invalid">{e}</Form.Control.Feedback>
Expand Down
74 changes: 0 additions & 74 deletions samples/contractupdate/src/useContractSchemaRpc.ts

This file was deleted.

0 comments on commit a045f3b

Please sign in to comment.