This is Picasso IBC sdk for cosmos, ethereum, solana, and polkadot(will be updated soon)
NOTE: To use the indexer API, you need to obtain the Hasura endpoint and secret key. Please contact the Picasso team for assistance.
npm install react graphql graphql-tag subscriptions-transport-ws picasso-sdk
- Please get hasura url and
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 };
};
const Stepper = () => {
const { ibcEvent, hopIndex, resetStatus } = usePicassoStatus('txHash...');
return <div>stepper..</div>;
};
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>
</>
);
};
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>
</>
);
};
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>
</>
);
};