Skip to content

Commit

Permalink
Ismp integration (#106)
Browse files Browse the repository at this point in the history
* ismp integration

* works

* progress

* fix encoding

* working

* all good

* integration

* working

* working

* merge

* show correct data

* working

* working well

* lint fixes

* readme

* on_timeout

* improvements

* last fixes
  • Loading branch information
Szegoo authored May 18, 2024
1 parent 3813a62 commit 03363a2
Show file tree
Hide file tree
Showing 14 changed files with 637 additions and 93 deletions.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,20 @@ The environment variables are automatically set in the Dockerfile; however, if y
2. To build an image run: `docker build -t corehub .`
3. To run the app: `docker run -dp 3000:3000 corehub`
4. Go to `http://localhost:3000/` to interact with the webapp

### Testing cross-chain region transfers

1. Build the [RegionX-Node](https://github.com/RegionX-Labs/RegionX-Node)
2. Follow the [Getting started with zombienet](https://github.com/RegionX-Labs/Coretime-Mock?tab=readme-ov-file#getting-started-with-zombienet) steps and run `npm run zombienet`
3. Set up the environment by running the [initialization script](https://github.com/RegionX-Labs/Coretime-Mock?tab=readme-ov-file#example-setting-up-the-full-environment) after the parachains started producing blocks.
4. Set the environment variables to the following:
```.env
WS_KUSAMA_RELAY_CHAIN="ws://127.0.0.1:9900"
WS_KUSAMA_CORETIME_CHAIN="ws://127.0.0.1:9910"
WS_REGIONX_CHAIN="ws://127.0.0.1:9920"
EXPERIMENTAL=true
```
5. Run `npm run dev`
6. Purchase a core from the bulk market
7. Cross chain transfer the core from the Coretime chain to the RegionX parachain.
8. Upon successful transfer the region record should be set on the RegionX parachain, and you should see the region location set to `RegionX chain`.
138 changes: 136 additions & 2 deletions src/components/Elements/Region/IsmpRegionCard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,15 @@ import en from 'javascript-time-ago/locale/en';
import { useCallback, useEffect, useState } from 'react';

import { timesliceToTimestamp } from '@/utils/functions';
import { makeResponse, makeTimeout, queryRequest } from '@/utils/ismp';

import { useRelayApi } from '@/contexts/apis';
import { useAccounts } from '@/contexts/account';
import { useCoretimeApi, useRelayApi } from '@/contexts/apis';
import { useRegionXApi } from '@/contexts/apis/RegionXApi';
import { ApiState } from '@/contexts/apis/types';
import { useCommon } from '@/contexts/common';
import { useRegions } from '@/contexts/regions';
import { useToast } from '@/contexts/toast';
import { ISMPRecordStatus, RegionMetadata } from '@/models';

import styles from './index.module.scss';
Expand All @@ -35,13 +40,25 @@ export const IsmpRegionCard = ({ regionMetadata }: IsmpRegionProps) => {
const {
state: { api: relayApi, apiState: relayApiState },
} = useRelayApi();
const {
state: { api: coretimeApi, apiState: coretimeApiState },
} = useCoretimeApi();
const {
state: { api: regionxApi, apiState: regionxApiState },
} = useRegionXApi();
const {
state: { activeAccount, activeSigner },
} = useAccounts();

const { fetchRegions } = useRegions();

const theme = useTheme();

const [beginTimestamp, setBeginTimestamp] = useState(0);

const { region, coreOccupancy, status } = regionMetadata;
const { timeslicePeriod } = useCommon();
const { toastWarning, toastSuccess, toastInfo, toastError } = useToast();

const setTimestamps = useCallback(
(api: ApiPromise) => {
Expand All @@ -59,6 +76,121 @@ export const IsmpRegionCard = ({ regionMetadata }: IsmpRegionProps) => {

setTimestamps(relayApi);
}, [relayApi, relayApiState, setTimestamps]);

useEffect(() => {
if (
!coretimeApi ||
coretimeApiState !== ApiState.READY ||
!regionxApi ||
regionxApiState !== ApiState.READY ||
!activeAccount
) {
return;
}

const onError = () =>
toastWarning(
`Failed to fulfill ISMP request. Wait 5 minutes to re-request`
);

const respond = async (commitment: string) => {
try {
const request: any = await queryRequest(regionxApi, commitment);
const currentTimestamp = (
await regionxApi.query.timestamp.now()
).toJSON() as number;

if (
request.get &&
currentTimestamp / 1000 > request.get.timeout_timestamp
) {
await makeTimeout(regionxApi, request, {
ready: () => toastInfo('Request timed out'),
inBlock: () => {
/* */
},
finalized: () => {
/* */
},
success: () => {
fetchRegions();
},
error: () => {
/* */
},
});
fetchRegions();
} else {
await makeResponse(
regionxApi,
coretimeApi,
request,
activeAccount.address,
{
ready: () => toastInfo('Fetching region record.'),
inBlock: () => toastInfo(`In Block`),
finalized: () => {
/* */
},
success: () => {
toastSuccess('Region record fetched.');
fetchRegions();
},
error: () => {
toastError(`Failed to fetch region record.`);
},
}
);
}
} catch {
onError();
}
};

if (status === ISMPRecordStatus.PENDING) {
regionMetadata.requestCommitment
? respond(regionMetadata.requestCommitment)
: onError();
}
}, [coretimeApi, regionxApi, coretimeApiState, regionxApiState]);

const requestRegionRecord = async () => {
if (
!regionxApi ||
regionxApiState !== ApiState.READY ||
!activeAccount ||
!activeSigner
) {
return;
}

const request = regionxApi.tx.regions.requestRegionRecord(
region.getRegionId()
);
try {
await request.signAndSend(
activeAccount.address,
{ signer: activeSigner },
({ status, events }) => {
if (status.isReady) toastInfo('Transaction was initiated');
else if (status.isInBlock) toastInfo(`In Block`);
else if (status.isFinalized) {
events.forEach(({ event: { method } }) => {
if (method === 'ExtrinsicSuccess') {
toastSuccess('Transaction successful');
fetchRegions();
} else if (method === 'ExtrinsicFailed') {
toastError(`Failed to request region record`);
}
});
}
}
);
} catch (e) {
toastError(`Failed to interlace the region. ${e}`);
}
};

return (
<Paper className={styles.container}>
<Box
Expand Down Expand Up @@ -129,7 +261,9 @@ export const IsmpRegionCard = ({ regionMetadata }: IsmpRegionProps) => {
Failed to fetch region record
</Typography>
</Stack>
<Button variant='outlined'>Request again</Button>
<Button variant='outlined' onClick={requestRegionRecord}>
Request again
</Button>
</>
) : (
<></>
Expand Down
46 changes: 43 additions & 3 deletions src/components/Modals/Regions/Transfer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,16 @@ import { Region } from 'coretime-utils';
import { useEffect, useState } from 'react';

import { isValidAddress } from '@/utils/functions';
import { transferRegionOnCoretimeChain } from '@/utils/transfers/native';
import {
transferRegionOnCoretimeChain,
transferRegionOnRegionXChain,
} from '@/utils/transfers/native';

import { ProgressButton, RegionOverview } from '@/components/Elements';

import { useAccounts } from '@/contexts/account';
import { useCoretimeApi } from '@/contexts/apis';
import { useRegionXApi } from '@/contexts/apis/RegionXApi';
import { useRegions } from '@/contexts/regions';
import { useToast } from '@/contexts/toast';
import { RegionLocation, RegionMetadata } from '@/models';
Expand Down Expand Up @@ -47,17 +51,53 @@ export const TransferModal = ({
const {
state: { api: coretimeApi },
} = useCoretimeApi();
const {
state: { api: regionxApi },
} = useRegionXApi();

const [newOwner, setNewOwner] = useState('');
const [working, setWorking] = useState(false);

const onTransfer = () => {
if (regionMetadata.location === RegionLocation.CORETIME_CHAIN) {
transferCoretimeRegion(regionMetadata.region);
transferOnCoretimeChain(regionMetadata.region);
} else if (regionMetadata.location === RegionLocation.REGIONX_CHAIN) {
transferOnRegionXChain(regionMetadata.region);
}
};

const transferCoretimeRegion = async (region: Region) => {
const transferOnRegionXChain = (region: Region) => {
if (!regionxApi || !activeAccount || !activeSigner) return;

if (!newOwner) {
toastError('Please input the new owner.');
return;
}

transferRegionOnRegionXChain(
regionxApi,
region,
activeSigner,
activeAccount.address,
newOwner,
{
ready: () => toastInfo('Transaction was initiated.'),
inBlock: () => toastInfo(`In Block`),
finalized: () => setWorking(false),
success: () => {
toastSuccess('Successfully transferred the region.');
onClose();
fetchRegions();
},
error: () => {
toastError(`Failed to transfer the region.`);
setWorking(false);
},
}
);
};

const transferOnCoretimeChain = async (region: Region) => {
if (!coretimeApi || !activeAccount || !activeSigner) return;
if (!newOwner) {
toastError('Please input the new owner.');
Expand Down
56 changes: 55 additions & 1 deletion src/contexts/apis/RegionXApi/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,60 @@ const types = {
owner: 'AccountId',
paid: 'Option<Balance>',
},
HashAlgorithm: {
_enum: ['Keccak', 'Blake2'],
},
StateMachineProof: {
hasher: 'HashAlgorithm',
storage_proof: 'Vec<Vec<u8>>',
},
SubstrateStateProof: {
_enum: {
OverlayProof: 'StateMachineProof',
StateProof: 'StateMachineProof',
},
},
LeafIndexQuery: {
commitment: 'H256',
},
StateMachine: {
_enum: {
Ethereum: {},
Polkadot: 'u32',
Kusama: 'u32',
},
},
Post: {},
Get: {
source: 'StateMachine',
dest: 'StateMachine',
nonce: 'u64',
from: 'Vec<u8>',
keys: 'Vec<Vec<u8>>',
height: 'u64',
timeout_timestamp: 'u64',
},
Request: {
_enum: {
Post: 'Post',
Get: 'Get',
},
},
};

const customRpc = {
ismp: {
queryRequests: {
description: '',
params: [
{
name: 'query',
type: 'Vec<LeafIndexQuery>',
},
],
type: 'Vec<Request>',
},
},
};

const defaultValue = {
Expand Down Expand Up @@ -53,7 +107,7 @@ const RegionXApiContextProvider = (props: any) => {
//
// For this reason we don't have different urls based on the network. However, this
// should be updated once this is in production.
connect(state, WS_REGIONX_CHAIN, dispatch, false, types);
connect(state, WS_REGIONX_CHAIN, dispatch, false, types, customRpc);
}, [state]);

return (
Expand Down
9 changes: 7 additions & 2 deletions src/contexts/apis/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,20 @@ export const connect = (
socket: string,
dispatch: any,
newSocket: boolean,
types?: any
types?: any,
customRpc?: any
) => {
const { apiState, jsonrpc } = state;

// We only want this function to be performed once
if (apiState !== ApiState.DISCONNECTED && !newSocket) return;

const provider = new WsProvider(socket);
const _api = new ApiPromise({ provider, rpc: jsonrpc, types });
const _api = new ApiPromise({
provider,
rpc: { ...jsonrpc, ...customRpc },
types,
});
dispatch({ type: 'CONNECT_INIT', socket });

// Set listeners for disconnection and reconnection event.
Expand Down
Loading

0 comments on commit 03363a2

Please sign in to comment.