Implement feature to add custom network config to embedded wallet (#59)

Part of https://www.notion.so/Laconic-Mainnet-Plan-1eca6b22d47280569cd0d1e6d711d949

Co-authored-by: Shreerang Kale <shreerangkale@gmail.com>
Co-authored-by: Prathamesh Musale <prathamesh.musale0@gmail.com>
Reviewed-on: cerc-io/snowballtools-base#59
Co-authored-by: Nabarun <nabarun@deepstacksoft.com>
Co-committed-by: Nabarun <nabarun@deepstacksoft.com>
This commit is contained in:
Nabarun 2025-06-24 17:06:51 +00:00 committed by nabarun
parent cda6ebec30
commit dd1d747b60
19 changed files with 189 additions and 40 deletions

View File

@ -47,11 +47,11 @@ jobs:
cat > packages/deployer/config.yml <<EOF
services:
registry:
rpcEndpoint: https://laconicd-sapo.laconic.com
gqlEndpoint: https://laconicd-sapo.laconic.com/api
rpcEndpoint: https://laconicd-mainnet-1.laconic.com
gqlEndpoint: https://laconicd-mainnet-1.laconic.com/api
userKey: $REGISTRY_USER_KEY
bondId: $REGISTRY_BOND_ID
chainId: laconic-testnet-2
chainId: laconic-mainnet
gasPrice: 0.001alnt
EOF

View File

@ -15,7 +15,6 @@ VITE_GITHUB_CLIENT_ID = 'LACONIC_HOSTED_CONFIG_github_clientid'
VITE_GITHUB_PWA_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_github_pwa_templaterepo'
VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo'
VITE_GITHUB_NEXT_APP_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_github_next_app_templaterepo'
VITE_LACONICD_CHAIN_ID = 'LACONIC_HOSTED_CONFIG_laconicd_chain_id'
VITE_WALLET_IFRAME_URL = 'LACONIC_HOSTED_CONFIG_wallet_iframe_url'
VITE_LIT_RELAY_API_KEY = 'LACONIC_HOSTED_CONFIG_lit_relay_api_key'
VITE_BUGSNAG_API_KEY = 'LACONIC_HOSTED_CONFIG_bugsnag_api_key'

View File

@ -14,5 +14,6 @@
"prepare": "husky install",
"build": "lerna run build --stream",
"lint": "lerna run lint --stream"
}
}
},
"packageManager": "yarn@1.22.19+sha1.4ba7fc5c6e704fce2066ecbfb0b0d8976fe62447"
}

View File

@ -1,8 +1,8 @@
services:
registry:
rpcEndpoint: https://laconicd-sapo.laconic.com
gqlEndpoint: https://laconicd-sapo.laconic.com/api
rpcEndpoint: https://laconicd-mainnet-1.laconic.com
gqlEndpoint: https://laconicd-mainnet-1.laconic.com/api
userKey:
bondId:
chainId: laconic-testnet-2
chainId: laconic-mainnet
gasPrice: 0.001alnt

View File

@ -133,7 +133,6 @@ record:
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: laconic-templates/test-progressive-web-app
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: laconic-templates/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_github_next_app_templaterepo: laconic-templates/starter.nextjs-react-tailwind
LACONIC_HOSTED_CONFIG_laconicd_chain_id: laconic-testnet-2
LACONIC_HOSTED_CONFIG_wallet_iframe_url: https://wallet.laconic.com
meta:
note: Added @ $CURRENT_DATE_TIME

View File

@ -12,5 +12,4 @@ VITE_BUGSNAG_API_KEY=
VITE_PASSKEY_WALLET_RPID=
VITE_TURNKEY_API_BASE_URL=
VITE_LACONICD_CHAIN_ID=
VITE_WALLET_IFRAME_URL=

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -0,0 +1,11 @@
{
"chainId": "laconic-mainnet",
"networkName": "laconicd mainnet",
"namespace": "cosmos",
"rpcUrl": "https://laconicd-mainnet-1.laconic.com",
"blockExplorerUrl": "",
"nativeDenom": "alnt",
"addressPrefix": "laconic",
"coinType": 118,
"gasPrice": 0.001
}

View File

@ -3,9 +3,10 @@ import { useCallback, useEffect } from 'react';
import { Box, Modal } from '@mui/material';
import {
VITE_LACONICD_CHAIN_ID,
VITE_WALLET_IFRAME_URL,
} from 'utils/constants';
import { REQUEST_WALLET_ACCOUNTS, WALLET_ACCOUNTS_DATA } from '../../../constants';
import { useAddNetwork } from '../../../hooks/useAddNetwork';
const ApproveTransactionModal = ({
setAccount,
@ -16,15 +17,17 @@ const ApproveTransactionModal = ({
setIsDataReceived: (isReceived: boolean) => void;
isVisible: boolean;
}) => {
const { setIframe, isNetworkAvailable, networkData } = useAddNetwork();
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
if (event.origin !== VITE_WALLET_IFRAME_URL) return;
if (event.data.type === 'WALLET_ACCOUNTS_DATA') {
if (event.data.type === WALLET_ACCOUNTS_DATA) {
setIsDataReceived(true);
if (event.data.data.length === 0) {
console.error(`Accounts not present for chainId: ${VITE_LACONICD_CHAIN_ID}`);
console.error(`Accounts not present for chainId: ${networkData?.chainId}`);
return;
}
@ -41,9 +44,14 @@ const ApproveTransactionModal = ({
return () => {
window.removeEventListener('message', handleMessage);
};
}, []);
}, [networkData]);
const getDataFromWallet = useCallback(() => {
if (!networkData) {
console.error('networkData should not be empty');
return;
}
const iframe = document.getElementById('walletIframe') as HTMLIFrameElement;
if (!iframe.contentWindow) {
@ -53,12 +61,18 @@ const ApproveTransactionModal = ({
iframe.contentWindow.postMessage(
{
type: 'REQUEST_WALLET_ACCOUNTS',
chainId: VITE_LACONICD_CHAIN_ID,
type: REQUEST_WALLET_ACCOUNTS,
chainId: networkData.chainId,
},
VITE_WALLET_IFRAME_URL,
);
}, []);
}, [networkData]);
useEffect(() => {
if (isNetworkAvailable) {
getDataFromWallet();
}
}, [isNetworkAvailable, getDataFromWallet])
return (
<Modal open={isVisible} disableEscapeKeyDown keepMounted>
@ -80,7 +94,7 @@ const ApproveTransactionModal = ({
}}
>
<iframe
onLoad={getDataFromWallet}
onLoad={(event) => setIframe(event.target as HTMLIFrameElement)}
id="walletIframe"
src={`${VITE_WALLET_IFRAME_URL}/wallet-embed`}
width="100%"

View File

@ -1,9 +1,10 @@
import { useEffect, useState } from 'react';
import { useEffect } from 'react';
import { Modal } from '@mui/material';
import { VITE_WALLET_IFRAME_URL } from 'utils/constants';
import useCheckBalance from '../../../hooks/useCheckBalance';
import { useAddNetwork } from '../../../hooks/useAddNetwork';
const CHECK_BALANCE_INTERVAL = 5000;
const IFRAME_ID = 'checkBalanceIframe';
@ -22,20 +23,19 @@ const CheckBalanceIframe = ({
IFRAME_ID,
);
const [isLoaded, setIsLoaded] = useState(false);
const { isNetworkAvailable, setIframe } = useAddNetwork();
useEffect(() => {
if (!isLoaded) {
if (!isNetworkAvailable || isBalanceSufficient) {
return;
}
checkBalance();
}, [amount, checkBalance, isLoaded]);
useEffect(() => {
if (!isPollingEnabled || !isLoaded || isBalanceSufficient) {
if (!isPollingEnabled) {
return;
}
const interval = setInterval(() => {
checkBalance();
}, CHECK_BALANCE_INTERVAL);
@ -43,7 +43,7 @@ const CheckBalanceIframe = ({
return () => {
clearInterval(interval);
};
}, [isBalanceSufficient, isPollingEnabled, checkBalance, isLoaded]);
}, [isBalanceSufficient, isPollingEnabled, checkBalance, isNetworkAvailable]);
useEffect(() => {
onBalanceChange(isBalanceSufficient);
@ -52,7 +52,7 @@ const CheckBalanceIframe = ({
return (
<Modal open={false} disableEscapeKeyDown keepMounted>
<iframe
onLoad={() => setIsLoaded(true)}
onLoad={(event) => setIframe(event.target as HTMLIFrameElement)}
id={IFRAME_ID}
src={VITE_WALLET_IFRAME_URL}
width="100%"

View File

@ -25,10 +25,10 @@ import ApproveTransactionModal from './ApproveTransactionModal';
import EnvironmentVariablesForm from 'pages/org-slug/projects/id/settings/EnvironmentVariablesForm';
import { EnvironmentVariablesFormValues } from 'types/types';
import {
VITE_LACONICD_CHAIN_ID,
VITE_WALLET_IFRAME_URL,
} from 'utils/constants';
import CheckBalanceIframe from './CheckBalanceIframe';
import { useAddNetwork } from '../../../hooks/useAddNetwork';
type ConfigureDeploymentFormValues = {
option: string;
@ -71,6 +71,7 @@ const Configure = () => {
const navigate = useNavigate();
const { toast, dismiss } = useToast();
const client = useGQLClient();
const { networkData } = useAddNetwork()
const methods = useForm<ConfigureFormValues>({
defaultValues: {
@ -421,7 +422,7 @@ const Configure = () => {
iframe.contentWindow.postMessage(
{
type: 'REQUEST_TX',
chainId: VITE_LACONICD_CHAIN_ID,
chainId: networkData?.chainId,
fromAddress: sender,
toAddress: recipient,
amount,

View File

@ -43,6 +43,11 @@ export const AuctionCard = ({ project }: { project: Project }) => {
const checkAuctionStatus = useCallback(async () => {
const result = await client.getAuctionData(project.auctionId);
if (!result) {
return
}
setAuctionStatus(result.status);
setAuctionDetails(result);
}, [project.auctionId, project.deployers, project.fundsReleased]);

View File

@ -6,6 +6,7 @@ import { useNavigate } from 'react-router-dom';
import { Box, Modal } from '@mui/material';
import { BASE_URL, VITE_WALLET_IFRAME_URL } from 'utils/constants';
import { REQUEST_CREATE_OR_GET_ACCOUNTS, WALLET_ACCOUNTS_DATA } from '../../../constants';
const axiosInstance = axios.create({
baseURL: BASE_URL,
@ -91,7 +92,7 @@ const AutoSignInIFrameModal = () => {
const handleAccountsDataResponse = async (event: MessageEvent) => {
if (event.origin !== VITE_WALLET_IFRAME_URL) return;
if (event.data.type === 'WALLET_ACCOUNTS_DATA') {
if (event.data.type === WALLET_ACCOUNTS_DATA) {
setAccountAddress(event.data.data[0]);
}
};
@ -115,7 +116,7 @@ const AutoSignInIFrameModal = () => {
iframe.contentWindow.postMessage(
{
type: 'REQUEST_CREATE_OR_GET_ACCOUNTS',
type: REQUEST_CREATE_OR_GET_ACCOUNTS,
chainId: '1',
},
VITE_WALLET_IFRAME_URL,

View File

@ -3,3 +3,14 @@ export const SHORT_COMMIT_HASH_LENGTH = 8;
export const SERVER_GQL_PATH = 'graphql';
export const SHOPIFY_APP_URL = 'https://store.laconic.com';
// iframe request types
export const REQUEST_CREATE_OR_GET_ACCOUNTS = 'REQUEST_CREATE_OR_GET_ACCOUNTS';
export const REQUEST_ADD_NETWORK = 'REQUEST_ADD_NETWORK';
export const REQUEST_WALLET_ACCOUNTS = 'REQUEST_WALLET_ACCOUNTS';
// iframe response types
export const WALLET_ACCOUNTS_DATA = 'WALLET_ACCOUNTS_DATA';
export const NETWORK_ADDED_RESPONSE = "NETWORK_ADDED_RESPONSE";
export const NETWORK_ALREADY_EXISTS_RESPONSE = "NETWORK_ALREADY_EXISTS_RESPONSE";
export const NETWORK_ADD_FAILED_RESPONSE = "NETWORK_ADD_FAILED_RESPONSE";

View File

@ -0,0 +1,92 @@
import { useState, useEffect } from 'react';
import { VITE_WALLET_IFRAME_URL } from 'utils/constants';
import { NETWORK_ADD_FAILED_RESPONSE, NETWORK_ADDED_RESPONSE, NETWORK_ALREADY_EXISTS_RESPONSE, REQUEST_ADD_NETWORK } from '../constants';
interface NetworkData {
chainId: string;
namespace: string;
networkName: string;
rpcUrl: string;
coinType: string;
addressPrefix?: string;
blockExplorerUrl?: string;
nativeDenom?: string;
gasPrice?: string;
}
export const useAddNetwork = () => {
const [networkData, setNetworkData] = useState<NetworkData | null>(null);
const [iframe, setIframe] = useState<HTMLIFrameElement | null>(null);
const [isNetworkAvailable, setIsNetworkAvailable] = useState(false);
// useEffect to add network in embedded wallet
useEffect(() => {
if (!networkData) {
return;
}
if (!iframe?.contentWindow) {
return;
}
iframe.contentWindow.postMessage(
{
type: REQUEST_ADD_NETWORK,
chainId: networkData.chainId,
networkData,
},
VITE_WALLET_IFRAME_URL,
);
}, [networkData, iframe]);
// useEffect to listen for network add reponses
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
if (event.origin !== VITE_WALLET_IFRAME_URL) return;
switch (event.data.type) {
case NETWORK_ADDED_RESPONSE:
case NETWORK_ALREADY_EXISTS_RESPONSE:
// Once network is available, set state
setIsNetworkAvailable(true);
break;
case NETWORK_ADD_FAILED_RESPONSE:
setIsNetworkAvailable(false);
console.error("Network could not be added:", event.data.message);
break;
default:
break;
}
};
window.addEventListener('message', handleMessage);
return () => {
window.removeEventListener('message', handleMessage);
};
}, []);
useEffect(() => {
const loadNetworkData = async () => {
try {
const res = await fetch('/network.json');
const json = await res.json();
setNetworkData(json);
} catch (err) {
console.error('Failed to load network data:', err);
}
};
loadNetworkData();
}, []);
return {
networkData,
isNetworkAvailable,
iframe,
setIframe
};
};

View File

@ -1,11 +1,17 @@
import { useState, useEffect, useCallback } from 'react';
import { VITE_LACONICD_CHAIN_ID } from 'utils/constants';
import { useAddNetwork } from './useAddNetwork';
const useCheckBalance = (amount: string, iframeId: string) => {
const [isBalanceSufficient, setIsBalanceSufficient] = useState<boolean>();
const { networkData } = useAddNetwork()
const checkBalance = useCallback(() => {
if (!networkData) {
return;
}
const iframe = document.getElementById(iframeId) as HTMLIFrameElement;
if (!iframe || !iframe.contentWindow) {
@ -16,12 +22,12 @@ const useCheckBalance = (amount: string, iframeId: string) => {
iframe.contentWindow.postMessage(
{
type: 'CHECK_BALANCE',
chainId: VITE_LACONICD_CHAIN_ID,
chainId: networkData.chainId,
amount,
},
import.meta.env.VITE_WALLET_IFRAME_URL
);
}, [iframeId, amount]);
}, [iframeId, amount, networkData]);
useEffect(() => {
const handleMessage = (event: MessageEvent) => {

View File

@ -204,7 +204,7 @@ const OverviewTabPanel = () => {
{project.deployments &&
project.deployments.length > 0 &&
project.deployments.map((deployment) => (
<div className="flex gap-2 items-center">
<div key={deployment.id} className="flex gap-2 items-center">
<Link to={deployment.applicationDeploymentRecordData.url}>
<span className="text-controls-primary dark:text-foreground group hover:border-controls-primary transition-colors border-b border-b-transparent flex gap-2 items-center text-sm tracking-tight">
{deployment.applicationDeploymentRecordData.url}

View File

@ -10,5 +10,4 @@ export const VITE_GITHUB_NEXT_APP_TEMPLATE_REPO = import.meta.env
export const VITE_GITHUB_CLIENT_ID = import.meta.env.VITE_GITHUB_CLIENT_ID;
export const VITE_BUGSNAG_API_KEY = import.meta.env.VITE_BUGSNAG_API_KEY;
export const VITE_LIT_RELAY_API_KEY = import.meta.env.VITE_LIT_RELAY_API_KEY;
export const VITE_LACONICD_CHAIN_ID = import.meta.env.VITE_LACONICD_CHAIN_ID;
export const VITE_WALLET_IFRAME_URL = import.meta.env.VITE_WALLET_IFRAME_URL;

View File

@ -414,14 +414,25 @@ export class GQLClient {
return data;
}
async getAuctionData(auctionId: string): Promise<types.Auction> {
const { data } = await this.client.query({
async getAuctionData(auctionId: string): Promise<types.Auction | null> {
const { data, errors } = await this.client.query({
query: queries.getAuctionData,
variables: {
auctionId,
},
});
if (errors && errors.length) {
const isAuctionNotFound = errors.some((error) =>
error.message?.includes('Auction not found')
);
if (isAuctionNotFound) {
return data;
}
}
return data.getAuctionData;
}