Skip to content

Picasso IBC sdk for cosmos, ethereum, solana, and polkadot

Notifications You must be signed in to change notification settings

ComposableFi/picasso-sdk

Repository files navigation

picasso-sdk

This is Picasso IBC sdk for cosmos, ethereum, solana, and polkadot(will be updated soon)

How to use indexer Query

NOTE: To use the indexer API, you need to obtain the Hasura endpoint and secret key. Please contact the Picasso team for assistance.

1. Install Packages

npm install react graphql graphql-tag subscriptions-transport-ws picasso-sdk

  • Please get hasura url and

2. Code example with react

usePicassoStatus.ts

import { useEffect, useState } from 'react';

import { type DocumentNode } from 'graphql';
import gql from 'graphql-tag';
import { type IbcEventsResponse } from 'picasso-sdk';
import { SubscriptionClient } from 'subscriptions-transport-ws';

type QueryKey = {
	fromBlockHash?: { _eq: string };
	sequence?: { _eq: number };
};

const HASURA_GRAPHQL_ENDPOINT = process.env.NEXT_PUBLIC_HASURA_URL || '';
const HASURA_ADMIN_SECRET = process.env.NEXT_PUBLIC_HASURA_PRIVATE_KEY || '';

const subscriptionQueryWithTxHash = gql`
	subscription MySubscription(
		$txHash: String!
		$fromBlockHash: String_comparison_exp = {}
		$sequence: String_comparison_exp = {}
	) {
		IbcEvents(where: { data: { _contains: { txHash: $txHash } }, fromBlockHash: $fromBlockHash, sequence: $sequence }) {
			data
			fromAssetId
			fromAmount
			fromAddress
			fromBlockHash
			fromChainId
			fromFee
			fromFeeAssetId
			fromTimestamp
			nextSequence
			sequence
			sourceChannel
			status
			timeout
			toAddress
			toAmount
			toAssetId
			toBlockHash
			toChainId
			toFee
			toFeeAssetId
			updatedAt
			toTimestamp
			type
			timeout_height
		}
	}
`;

const subscriptionQueryWithoutTxHash = gql`
	subscription MySubscription($fromBlockHash: String_comparison_exp = {}, $sequence: String_comparison_exp = {}) {
		IbcEvents(where: { fromBlockHash: $fromBlockHash, sequence: $sequence }) {
			data
			fromAssetId
			fromAmount
			fromAddress
			fromBlockHash
			fromChainId
			fromFee
			fromFeeAssetId
			fromTimestamp
			nextSequence
			sequence
			sourceChannel
			status
			timeout
			toAddress
			toAmount
			toAssetId
			toBlockHash
			toChainId
			toFee
			toFeeAssetId
			updatedAt
			toTimestamp
			type
			timeout_height
		}
	}
`;

export const usePicassoStatus = (txHash?: string, duration: number = 10000) => {
	const [ibcEvent, setIbcEvent] = useState<Partial<IbcEventsResponse>>();
	const [hopIndex, setHopIndex] = useState(-1);

	const resetStatus = () => {
		setIbcEvent(undefined);
		setHopIndex(-1);
	};

	const subscribeToIbcEvents = (
		client: SubscriptionClient,
		variables: QueryKey & { txHash?: string },
		subscriptionQuery: DocumentNode
	) => {
		if (hopIndex > 100) {
			client.close();
			return;
		}

		const subscription = client.request({ query: subscriptionQuery, variables }).subscribe({
			next(data) {
				console.log('Received data:', data);
				const event = data?.data?.IbcEvents?.[0];
				setIbcEvent(event);
				if (event?.fromBlockHash !== ibcEvent?.fromBlockHash) {
					setHopIndex(prev => prev + 1);
				}

				if (event?.toBlockHash && event?.nextSequence) {
					const nextVariables = {
						fromBlockHash: { _eq: event.toBlockHash },
						sequence: { _eq: event.nextSequence }
					};
					subscribeToIbcEvents(client, nextVariables, subscriptionQueryWithoutTxHash);
				} else if (event && ['TransferPending', 'send_packet'].every(v => event?.status !== v)) {
					client.close();

					console.log('Subscription stopped:', event);
				}
			},
			error(err) {
				console.error('Subscription error:', err);
			},
			complete() {
				console.log('Subscription complete');
			}
		});

		return subscription;
	};

	useEffect(() => {
		if (!txHash) return;
		resetStatus();
		const client = new SubscriptionClient(
			HASURA_GRAPHQL_ENDPOINT,
			{
				reconnect: true,
				connectionParams: {
					headers: {
						'x-hasura-admin-secret': HASURA_ADMIN_SECRET
					}
				}
			},
			WebSocket
		);

		const initialVariables = { txHash };
		const initialSubscription = subscribeToIbcEvents(client, initialVariables, subscriptionQueryWithTxHash);
		if (ibcEvent && ibcEvent?.status !== 'TransferPending' && ibcEvent?.status !== 'send_packet') {
			console.log('this has closed');
			client.close();

			initialSubscription?.unsubscribe();
			const timer = setTimeout(() => {
				hopIndex >= 0 && setHopIndex(-1);

				setIbcEvent(undefined);
			}, duration);

			return () => {
				clearTimeout(timer);
				console.log('Subscription stopped!');
			};
		}

		return () => {
			initialSubscription?.unsubscribe();
			client.close();
			clearTimeout(duration);
		};
	}, [txHash, duration]);

	return { hopIndex, ibcEvent, resetStatus };
};


Usage

const Stepper = () => {
	const { ibcEvent, hopIndex, resetStatus } = usePicassoStatus('txHash...');
	return <div>stepper..</div>;
};

How to use methods

Ethereum

const web3 = getWeb3('endpoint');

const AMOUNT = '10000000000000000'; // hardcoded example
// use txhash to track the transaction from indexer
const EthereumButton = () => {
	//example : send ETH osmosis -> solana transfer via picasso pfm

	// example txhash: ethereum -> picasso https://etherscan.io/tx/0x6c3fd9120cfe7825d98e41e5b71279cfd6543c3811d82e40545ca7e69a2d95ce
	const ethereumToPicassoTransfer = async () => {
		//TODO: add approval
		const txHash = await ethereumTransfer({
			web3,
			amount: AMOUNT, // wei amount
			assetId: 'ETH', // for PICA it is '0xbb63a9b64a80e9338b8ea298c51765e57c4f159c'
			originAddress: '0x46762bdE09C1a1c566c3efD959368455Bf20c354',
			destinationAddress: 'pica1ewm97t5qw3uutwd9qh0ydy007ymhl8qth56qlj',
			channel: 2, // etheruem to picasso(cosmos) channel is 2
			minimalDenom: 'ETH', // for PICA it is 'transfer/channel-2/ppica'
			memo: '',
			timeout: 240
		});
		console.log(txHash, 'txHash:Ethereum->Picasso');
	};

	// example txHash: ethereum -> archway https://etherscan.io/tx/0x332d9cd30af18245e5a70989f0e61a0f98594ca25baf159ab409223b808c4744
	// send PICA from ethereum to osmosis
	const ethereumPfmTransfer = async () => {
		const approvedAmount = await getApprovedErc20(
			web3,
			'0x46762bdE09C1a1c566c3efD959368455Bf20c354',
			'0xbb63a9b64a80e9338b8ea298c51765e57c4f159c'
		); // PICA's erc20 address
		if (new Big(approvedAmount || 0).lt(AMOUNT)) {
			await approveErc20({
				web3,
				account: '0xbb63a9b64a80e9338b8ea298c51765e57c4f159c',
				amount: AMOUNT,
				erc20TokenAddress: '0x3432b6a60d23ca0dfca7761b7ab56459d9c964d0' // FXS's erc20 address
			});
		}

		// ethereum memo should use escape code
		const memoWithEscape = memoBuilder({
			destChannel: 3, // picasso(cosmos)-> osmosis 's  source channel is 3
			destAddress: 'osmo1ewm97t5qw3uutwd9qh0ydy007ymhl8qtyn24d8' // osmosis address
		})
			.replace(/\\/g, '\\\\')
			.replace(/"/g, '\\"');
		// TODO: add approval
		const txHash = await ethereumTransfer({
			web3,
			amount: '1000000000000000', // wei amount
			assetId: 'ETH', // for PICA it is '0xbb63a9b64a80e9338b8ea298c51765e57c4f159c'
			originAddress: '0x46762bdE09C1a1c566c3efD959368455Bf20c354', // ethereum address
			destinationAddress: 'pica1ewm97t5qw3uutwd9qh0ydy007ymhl8qth56qlj', // picasso address
			channel: 2, // etheruem to picasso(cosmos) channel is 2
			minimalDenom: 'ETH', // for PICA it is 'transfer/channel-2/ppica'
			memo: memoWithEscape
		});

		console.log(txHash, 'txHash:Ethereum->Osmosis');
	};
	return (
		<>
			<button onClick={ethereumToPicassoTransfer}>ethereumToPicassoTransfer</button>
			<button onClick={ethereumPfmTransfer}>ethereumPfmTransfer</button>
		</>
	);
};

Solana

const SolanaButton = () => {
	//example : osmosis -> picasso transfer
	const solanaToPicassoTransfer = async () => {
		//rewrite below code to use solanaTransfer method

		const txHash = await solanaTransfer({
			//write the parameters here
			quantity: '1000000',
			accountId: '1ryziZbFQW4fcWck9wW4vU4KD4qxPHKhmAht6pXPFWo', // solana address
			destinationAddress: 'pica1ewm97t5qw3uutwd9qh0ydy007ymhl8qth56qlj', // picasso originAddress
			configAssetId: 'EpZeFE5ngedGVug3s1VQK9987FEsCY512VtKUKmfzidM', // Solana OSMO mintAddress
			sourceChannelId: 1, // solana -> picasso(cosmos) 's source channel is 1
			configDenom: 'transfer/channel-1/transfer/channel-3/uosmo',
			endpoint: 'https://mainnet.helius-rpc.com/?api-key=your-api-key', // helius rpc
			timeout: getTimeOut(30).toNumber(),
			memo: ''
		});
		console.log(txHash, 'txHash:Solana->Picasso');
	};

	// example: solana -> osmosis transfer via picasso pfm
	const solanaPfmTransfer = async () => {
		const txHash = await solanaTransfer({
			//write the parameters here
			quantity: '1000000',
			accountId: '1ryziZbFQW4fcWck9wW4vU4KD4qxPHKhmAht6pXPFWo', // solana address
			destinationAddress: 'pfm', // set pfm as hop address
			configAssetId: 'EpZeFE5ngedGVug3s1VQK9987FEsCY512VtKUKmfzidM', // Solana OSMO mintAddress
			sourceChannelId: 1, // solana -> picasso(cosmos) 's source channel is 1
			configDenom: 'transfer/channel-1/transfer/channel-3/uosmo',
			endpoint: 'https://mainnet.helius-rpc.com/?api-key=your-api-key', // helius rpc
			timeout: getTimeOut(30).toNumber(),
			memo: memoBuilder({
				destChannel: 3, // picasso(cosmos)-> osmosis 's  source channel is 3
				destAddress: 'osmo1ewm97t5qw3uutwd9qh0ydy007ymhl8qtyn24d8' // osmosis address
			}) // memo for pfm
		});
		console.log(txHash, 'txHash:Solana->Osmosis');
	};

	return (
		<>
			<button onClick={solanaToPicassoTransfer}>solanaToPicassoTransfer</button>
			<button onClick={solanaPfmTransfer}>solanaPfmTransfer</button>
		</>
	);
};

Cosmos

const CosmosButton = () => {
	//example : osmosis -> solana transfer via picasso pfm
	const cosmosToSolanaTransfer = async () => {
		const txHash = await cosmosTransfer({
			sourceChannel: 1279,
			sourceAddress: 'osmo1ewm97t5qw3uutwd9qh0ydy007ymhl8qtyn24d8', //osmosis fromAddress
			destAddress: 'pica1ewm97t5qw3uutwd9qh0ydy007ymhl8qth56qlj', // picasso hopAddress
			amount: '100000000', // minimal denom amount
			assetId: 'uosmo', // coin or ibc denom
			fee: 'auto', // set fee as auto
			chainId: 'osmosis-1', // source chain id
			rpc: 'https://osmosis-rpc.polkachu.com', // use chain rpc from chain registry
			memo: memoBuilder({
				destChannel: 71, // picasso(cosmos)-> solana 's  source channel is 71
				destAddress: '1ryziZbFQW4fcWck9wW4vU4KD4qxPHKhmAht6pXPFWo' // solana address
			}) // memo for pfm
		});
		console.log(txHash, 'txHash:Osmosis->Solana');
	};

	// example : osmosis -> ethereum transfer via picasso pfm
	const cosmosToEthereumTransfer = async () => {
		const txHash = await cosmosTransfer({
			sourceChannel: 1279,
			sourceAddress: 'osmo1ewm97t5qw3uutwd9qh0ydy007ymhl8qtyn24d8', //osmosis fromAddress
			destAddress: 'pica1ewm97t5qw3uutwd9qh0ydy007ymhl8qth56qlj', // picasso hopAddress
			amount: '100000000', // minimal denom amount
			assetId: 'uosmo', // coin or ibc denom
			fee: 'auto', // set fee as auto
			chainId: 'osmosis-1', // source chain id
			rpc: 'https://osmosis-rpc.polkachu.com', // use chain rpc from chain registry
			memo: memoBuilder({
				destChannel: 52, //  picasso(cosmos)->ethereum's source channel is 52
				destAddress: '0x46762bdE09C1a1c566c3efD959368455Bf20c354' // ethereum address
			}) //memo for pfm
		});
		console.log(txHash, 'txHash:Osmosis->Ethereum');
	};
	return (
		<>
			<button onClick={cosmosToSolanaTransfer}>cosmosToSolanaTransfer</button>
			<button onClick={cosmosToEthereumTransfer}>cosmosToEthereumTransfer</button>
		</>
	);
};

About

Picasso IBC sdk for cosmos, ethereum, solana, and polkadot

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published