diff --git a/packages/cli/package.json b/packages/cli/package.json index e4ba2d2d..0d710b17 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -3,7 +3,7 @@ "description": "A quick and easy way to bootstrap your dApps on Cardano using Mesh.", "homepage": "https://meshjs.dev", "author": "MeshJS", - "version": "1.0.4", + "version": "1.0.5", "license": "Apache-2.0", "main": "dist/create-mesh-app.cjs.js", "bin": { diff --git a/packages/cli/src/actions/create.ts b/packages/cli/src/actions/create.ts index 8c58286f..06635d2a 100644 --- a/packages/cli/src/actions/create.ts +++ b/packages/cli/src/actions/create.ts @@ -17,7 +17,7 @@ export const create = async (name, options) => { { title: 'Starter Project', value: 'starter' }, { title: 'Multi-Sig Minting', value: 'minting' }, { title: 'Stake-Pool Website', value: 'staking' }, - { title: 'Smart-Contract Marketplace ', value: 'marketplace' }, + { title: 'Cardano Sign-In', value: 'signin' }, ])); const stack = diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index da674518..8417a347 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -47,7 +47,7 @@ const main = async () => { createOption( '-t, --template ', `The template to start your project from.` - ).choices(['starter', 'minting', 'staking', 'marketplace']) + ).choices(['starter', 'minting', 'staking', 'marketplace', 'signin']) ) .addOption( createOption( diff --git a/packages/demo/components/common/blockchainProvider.tsx b/packages/demo/components/common/blockchainProvider.tsx index cb322c82..55af4a59 100644 --- a/packages/demo/components/common/blockchainProvider.tsx +++ b/packages/demo/components/common/blockchainProvider.tsx @@ -9,7 +9,7 @@ export default function BlockchainProviderCodeSnippet() { codeBF += `const blockchainProvider = new BlockfrostProvider('');`; let codeKoios = `import { KoiosProvider } from '@meshsdk/core';\n\n`; - codeKoios += `const blockchainProvider = new KoiosProvider('');`; + codeKoios += `const blockchainProvider = new KoiosProvider('<'api'|'preview'|'preprod'|'guild'>');`; let codeTango = `import { TangoProvider } from '@meshsdk/core';\n\n`; codeTango += `const blockchainProvider = new TangoProvider(\n`; diff --git a/packages/demo/components/pages/providers/badges.tsx b/packages/demo/components/pages/providers/badges.tsx index e5a2fd25..2dcb760e 100644 --- a/packages/demo/components/pages/providers/badges.tsx +++ b/packages/demo/components/pages/providers/badges.tsx @@ -13,3 +13,19 @@ export function BadgeSubmitter() { ); } + +export function BadgeEvaluator() { + return ( + + Evaluator + + ); +} + +export function BadgeListener() { + return ( + + Listener + + ); +} diff --git a/packages/demo/components/pages/providers/evaluator.tsx b/packages/demo/components/pages/providers/evaluator.tsx new file mode 100644 index 00000000..359aec48 --- /dev/null +++ b/packages/demo/components/pages/providers/evaluator.tsx @@ -0,0 +1,42 @@ +import { useEffect, useState } from 'react'; +import SectionTwoCol from '../../common/sectionTwoCol'; +import { demoAddresses } from '../../../configs/demo'; +import { BadgeEvaluator } from './badges'; +import { evaluateTxLeft, evaluateTxRight } from './evaluator/evaluateTx'; +import { useWallet } from '@meshsdk/react'; + +export default function Evaluator({ evaluator, evaluatorName }) { + const { wallet, connected } = useWallet(); + const [address, setAddress] = useState(demoAddresses.testnet); + // const [lovelace, setLovelace] = useState('5000000'); + + useEffect(() => { + async function init() { + setAddress( + (await wallet.getNetworkId()) === 1 + ? demoAddresses.mainnet + : demoAddresses.testnet + ); + } + if (connected) { + init(); + } + }, [connected]); + + return ( + <> + } + /> + + ); +} diff --git a/packages/demo/components/pages/providers/evaluator/evaluateTx.tsx b/packages/demo/components/pages/providers/evaluator/evaluateTx.tsx new file mode 100644 index 00000000..0da43bf5 --- /dev/null +++ b/packages/demo/components/pages/providers/evaluator/evaluateTx.tsx @@ -0,0 +1,143 @@ +import { useState } from 'react'; +import RunDemoButton from '../../../common/runDemoButton'; +import RunDemoResult from '../../../common/runDemoResult'; +import Card from '../../../ui/card'; +import Codeblock from '../../../ui/codeblock'; +import { Transaction, KoiosProvider, resolveDataHash } from '@meshsdk/core'; +import { useWallet, CardanoWallet } from '@meshsdk/react'; + +export function evaluateTxLeft({ evaluatorName }) { + let code1 = ``; + code1 += `const unsignedTx = await tx.build();\n`; + code1 += `const evaluateTx = await ${evaluatorName}.evaluateTx(unsignedTx);\n`; + + let demoResults = ``; + demoResults += `[\n`; + demoResults += ` {\n`; + demoResults += ` "index": 0,\n`; + demoResults += ` "tag": "SPEND",\n`; + demoResults += ` "budget": {\n`; + demoResults += ` "mem": 1700,\n`; + demoResults += ` "steps": 368100\n`; + demoResults += ` }\n`; + demoResults += ` }\n`; + demoResults += `]\n`; + + let codeRedeemer = ``; + codeRedeemer += `const redeemer = {\n`; + codeRedeemer += ` data: { alternative: 0, fields: [...] },\n`; + codeRedeemer += ` budget: {\n`; + codeRedeemer += ` mem: 1700,\n`; + codeRedeemer += ` steps: 368100,\n`; + codeRedeemer += ` },\n`; + codeRedeemer += `};\n`; + + return ( + <> +

+ evaluateTx() accepts an unsigned transaction ( + unsignedTx) and it evaluates the resources required to + execute the transaction. Note that, this is only valid for transaction + interacting with redeemer (smart contract). By knowing the budget + required, you can use this to adjust the redeemer's budget so you don't + spend more than you need to execute transactions for this smart + contract. +

+ +

+ Example responses from unlocking assets from the always succeed smart + contract. +

+ +

+ With the mem and steps, you can refine the + budget for the redeemer. For example: +

+ + + ); +} + +export function evaluateTxRight({ evaluator }) { + const { wallet, connected } = useWallet(); + const [loading, setLoading] = useState(false); + const [response, setResponse] = useState(null); + const [responseError, setResponseError] = useState(null); + + async function _getAssetUtxo({ scriptAddress, asset, datum }) { + const koios = new KoiosProvider('preprod'); + + const utxos = await koios.fetchAddressUTxOs(scriptAddress, asset); + + const dataHash = resolveDataHash(datum); + + let utxo = utxos.find((utxo: any) => { + return utxo.output.dataHash == dataHash; + }); + + return utxo; + } + + async function runDemo() { + setLoading(true); + setResponse(null); + setResponseError(null); + + try { + const assetUtxo = await _getAssetUtxo({ + scriptAddress: + 'addr_test1wpnlxv2xv9a9ucvnvzqakwepzl9ltx7jzgm53av2e9ncv4sysemm8', + asset: + '64af286e2ad0df4de2e7de15f8ff5b3d27faecf4ab2757056d860a424d657368546f6b656e', + datum: 'supersecretmeshdemo', + }); + + const address = await wallet.getChangeAddress(); + + const tx = new Transaction({ initiator: wallet }) + .redeemValue({ + value: assetUtxo, + script: { + version: 'V1', + code: '4e4d01000033222220051200120011', + }, + datum: 'supersecretmeshdemo', + }) + .sendValue(address, assetUtxo) + .setRequiredSigners([address]); + + const unsignedTx = await tx.build(); + const evaluateTx = await evaluator.evaluateTx(unsignedTx); + + setResponse(evaluateTx); + } catch (error) { + setResponseError(`${error}`); + } + setLoading(false); + } + + return ( + <> + +

+ Unlock an asset from the always succeed to check how much it takes to + execute this transaction. +

+ {connected ? ( + <> + + + ) : ( + + )} + + + +
+ + ); +} diff --git a/packages/demo/components/pages/providers/fetcher.tsx b/packages/demo/components/pages/providers/fetcher.tsx index 54dd6b02..94c5ddba 100644 --- a/packages/demo/components/pages/providers/fetcher.tsx +++ b/packages/demo/components/pages/providers/fetcher.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import SectionTwoCol from '../../common/sectionTwoCol'; import { demoAddresses } from '../../../configs/demo'; import { BadgeFetcher } from './badges'; @@ -26,8 +26,16 @@ import { fetchHandleAddressLeft, fetchHandleAddressRight, } from './fetchers/fetchHandleAddress'; +import { useWallet } from '@meshsdk/react'; +import { fetchTxInfoLeft, fetchTxInfoRight } from './fetchers/fetchTxInfo'; +import { + fetchBlockInfoLeft, + fetchBlockInfoRight, +} from './fetchers/fetchBlockInfo'; export default function Fetcher({ fetcher, fetcherName }) { + const { wallet, connected } = useWallet(); + const [fetchAddressUtxosAddress, setfetchAddressUtxosAddress] = useState(demoAddresses.testnet); const [fetchAddressUtxosAsset, setfetchAddressUtxosAsset] = useState( @@ -51,6 +59,26 @@ export default function Fetcher({ fetcher, fetcherName }) { const [fetchHandleAddressHandle, setfetchHandleAddressHandle] = useState('jingles'); + const [txHash, setTxHash] = useState( + 'f4ec9833a3bf95403d395f699bc564938f3419537e7fb5084425d3838a4b6159' + ); + const [block, setBlock] = useState( + '79f60880b097ec7dabb81f75f0b52fedf5e922d4f779a11c0c432dcf22c56089' + ); + + // useEffect(() => { + // async function init() { + // setTxHash( + // (await wallet.getNetworkId()) === 1 + // ? '84a1d1a9f8fb3e7b4f3d1bb04ece750fe2697e74b7916804c2f179870eb34f17' + // : 'f4ec9833a3bf95403d395f699bc564938f3419537e7fb5084425d3838a4b6159' + // ); + // } + // if (connected) { + // init(); + // } + // }, [connected]); + return ( <> } /> + + } + /> + } /> + } /> + } /> + } + /> ); } diff --git a/packages/demo/components/pages/providers/fetchers/fetchAccountInfo.tsx b/packages/demo/components/pages/providers/fetchers/fetchAccountInfo.tsx index 1294b3b6..0de5e34b 100644 --- a/packages/demo/components/pages/providers/fetchers/fetchAccountInfo.tsx +++ b/packages/demo/components/pages/providers/fetchers/fetchAccountInfo.tsx @@ -16,6 +16,7 @@ export function fetchAccountInfoLeft({ fetcherName, fetchAccountInfoAddress }) { ); } + export function fetchAccountInfoRight({ fetcher, fetchAccountInfoAddress, diff --git a/packages/demo/components/pages/providers/fetchers/fetchBlockInfo.tsx b/packages/demo/components/pages/providers/fetchers/fetchBlockInfo.tsx new file mode 100644 index 00000000..8cac96b3 --- /dev/null +++ b/packages/demo/components/pages/providers/fetchers/fetchBlockInfo.tsx @@ -0,0 +1,59 @@ +import { useState } from 'react'; +import RunDemoButton from '../../../common/runDemoButton'; +import RunDemoResult from '../../../common/runDemoResult'; +import Card from '../../../ui/card'; +import Codeblock from '../../../ui/codeblock'; +import Input from '../../../ui/input'; + +export function fetchBlockInfoLeft({ fetcherName, block }) { + let code1 = `await ${fetcherName}.fetchBlockInfo(\n`; + code1 += ` '${block}',\n`; + code1 += `)`; + return ( + <> +

+ Fetch block infomation. You can get the hash from{' '} + fetchTxInfo(). +

+ + + ); +} + +export function fetchBlockInfoRight({ fetcher, block, setBlock }) { + const [loading, setLoading] = useState(false); + const [response, setResponse] = useState(null); + const [responseError, setResponseError] = useState(null); + async function runDemo() { + setLoading(true); + setResponse(null); + setResponseError(null); + try { + const res = await fetcher.fetchBlockInfo(block); + setResponse(res); + } catch (error) { + setResponseError(`${error}`); + } + setLoading(false); + } + return ( + <> + + setBlock(e.target.value)} + placeholder="Block hash" + label="Block hash" + /> + + + + + + + ); +} diff --git a/packages/demo/components/pages/providers/fetchers/fetchTxInfo.tsx b/packages/demo/components/pages/providers/fetchers/fetchTxInfo.tsx new file mode 100644 index 00000000..77f533ba --- /dev/null +++ b/packages/demo/components/pages/providers/fetchers/fetchTxInfo.tsx @@ -0,0 +1,59 @@ +import { useState } from 'react'; +import RunDemoButton from '../../../common/runDemoButton'; +import RunDemoResult from '../../../common/runDemoResult'; +import Card from '../../../ui/card'; +import Codeblock from '../../../ui/codeblock'; +import Input from '../../../ui/input'; + +export function fetchTxInfoLeft({ fetcherName, txHash }) { + let code1 = `await ${fetcherName}.fetchTxInfo(\n`; + code1 += ` '${txHash}',\n`; + code1 += `)`; + return ( + <> +

+ Fetch transaction infomation. Only confirmed transaction can be + retrieved. +

+ + + ); +} + +export function fetchTxInfoRight({ fetcher, txHash, setTxHash }) { + const [loading, setLoading] = useState(false); + const [response, setResponse] = useState(null); + const [responseError, setResponseError] = useState(null); + async function runDemo() { + setLoading(true); + setResponse(null); + setResponseError(null); + try { + const res = await fetcher.fetchTxInfo(txHash); + setResponse(res); + } catch (error) { + setResponseError(`${error}`); + } + setLoading(false); + } + return ( + <> + + setTxHash(e.target.value)} + placeholder="Transaction hash" + label="Transaction hash" + /> + + + + + + + ); +} diff --git a/packages/demo/components/pages/providers/listener.tsx b/packages/demo/components/pages/providers/listener.tsx new file mode 100644 index 00000000..19948365 --- /dev/null +++ b/packages/demo/components/pages/providers/listener.tsx @@ -0,0 +1,52 @@ +import { useEffect, useState } from 'react'; +import SectionTwoCol from '../../common/sectionTwoCol'; +import { demoAddresses } from '../../../configs/demo'; +import { BadgeListener } from './badges'; +import { + onTxConfirmedLeft, + onTxConfirmedRight, +} from './listener/onTxConfirmed'; +import { onNextTxLeft, onNextTxRight } from './listener/onNextTx'; +import { useWallet } from '@meshsdk/react'; + +export default function Listener({ listener, listenerName }) { + const { wallet, connected } = useWallet(); + const [address, setAddress] = useState(demoAddresses.testnet); + const [lovelace, setLovelace] = useState('5000000'); + + useEffect(() => { + async function init() { + setAddress( + (await wallet.getNetworkId()) === 1 + ? demoAddresses.mainnet + : demoAddresses.testnet + ); + } + if (connected) { + init(); + } + }, [connected]); + + return ( + <> + } + /> + + ); +} diff --git a/packages/demo/components/pages/providers/listener/onNextTx.tsx b/packages/demo/components/pages/providers/listener/onNextTx.tsx new file mode 100644 index 00000000..858ff2e1 --- /dev/null +++ b/packages/demo/components/pages/providers/listener/onNextTx.tsx @@ -0,0 +1,87 @@ +import { useEffect, useState } from 'react'; +import RunDemoButton from '../../../common/runDemoButton'; +import RunDemoResult from '../../../common/runDemoResult'; +import Card from '../../../ui/card'; +import Codeblock from '../../../ui/codeblock'; +import { Transaction } from '@meshsdk/core'; +import { useWallet, CardanoWallet } from '@meshsdk/react'; +import { demoAddresses } from '../../../../configs/demo'; + +export function onNextTxLeft({ listenerName }) { + let code1 = ``; + code1 += `${listenerName}.onNextTx((tx) => {\n`; + code1 += ` console.log('onNextTx', tx);\n`; + code1 += `});\n`; + + return ( + <> +

Listen for transactions that are submitted to the network.

+ + + ); +} + +export function onNextTxRight({ listener }) { + const { wallet, connected } = useWallet(); + + const [loading, setLoading] = useState(false); + const [response, setResponse] = useState(null); + const [responseError, setResponseError] = useState(null); + const [onNextTxLogs, setonNextTxLogs] = useState(''); + + async function runDemo() { + setLoading(true); + setResponse(null); + setResponseError(null); + + try { + const tx = new Transaction({ initiator: wallet }); + tx.sendLovelace(demoAddresses.testnet, '2000000'); + + const unsignedTx = await tx.build(); + const signedTx = await wallet.signTx(unsignedTx); + const txHash = await listener.submitTx(signedTx); + console.log('txHash', txHash); + + // setResponse(txHash); + } catch (error) { + setResponseError(`${error}`); + } + setLoading(false); + } + + useEffect(() => { + if (connected) { + listener.onNextTx((tx) => { + console.log(111, 'ogmiosProvider.onNextTx', tx); + const tmp = onNextTxLogs + `${tx}\n`; + setonNextTxLogs(tmp); + }); + } + }, [connected]); + + return ( + <> + + {connected ? ( + <> + +
onNextTx Logs
+ + + ) : ( + + )} + + {/* */} + + +
+ + ); +} diff --git a/packages/demo/components/pages/providers/listener/onTxConfirmed.tsx b/packages/demo/components/pages/providers/listener/onTxConfirmed.tsx new file mode 100644 index 00000000..21ed22e9 --- /dev/null +++ b/packages/demo/components/pages/providers/listener/onTxConfirmed.tsx @@ -0,0 +1,133 @@ +import { useState } from 'react'; +import RunDemoButton from '../../../common/runDemoButton'; +import RunDemoResult from '../../../common/runDemoResult'; +import Card from '../../../ui/card'; +import Codeblock from '../../../ui/codeblock'; +import Input from '../../../ui/input'; +import { Transaction } from '@meshsdk/core'; +import { useWallet, CardanoWallet } from '@meshsdk/react'; +import { ArrowPathIcon, CheckIcon } from '@heroicons/react/24/solid'; + +export function onTxConfirmedLeft({ listenerName, address, lovelace }) { + let code1 = `const tx = new Transaction({ initiator: wallet });\n`; + code1 += `tx.sendLovelace('${address}', '${lovelace}');\n`; + code1 += `\n`; + code1 += `const unsignedTx = await tx.build();\n`; + code1 += `const signedTx = await wallet.signTx(unsignedTx);\n`; + code1 += `const txHash = await wallet.submitTx(signedTx);\n`; + code1 += `\n`; + code1 += `${listenerName}.onTxConfirmed(txHash, () => {\n`; + code1 += ` console.log('Transaction confirmed');\n`; + code1 += `});\n`; + + return ( + <> +

+ Allow you to listen to a transaction confirmation. Upon confirmation, + the callback will be called. +

+ + + ); +} + +export function onTxConfirmedRight({ + listener, + address, + setAddress, + lovelace, + setLovelace, +}) { + const { wallet, connected } = useWallet(); + + const [loading, setLoading] = useState(false); + const [response, setResponse] = useState(null); + const [responseError, setResponseError] = useState(null); + const [txWaitingConfrim, setTxWaitingConfrim] = + useState(null); + + async function runDemo() { + setLoading(true); + setResponse(null); + setResponseError(null); + setTxWaitingConfrim(null); + + try { + const tx = new Transaction({ initiator: wallet }); + tx.sendLovelace(address, lovelace); + + const unsignedTx = await tx.build(); + const signedTx = await wallet.signTx(unsignedTx); + const txHash = await wallet.submitTx(signedTx); + + setResponse(txHash); + + setTxWaitingConfrim(false); + listener.onTxConfirmed(txHash, () => { + setTxWaitingConfrim(true); + }); + } catch (error) { + setResponseError(`${error}`); + } + setLoading(false); + } + + return ( + <> + + setAddress(e.target.value)} + placeholder="Address" + label="Address" + /> + setLovelace(e.target.value)} + placeholder="Lovelace" + label="Lovelace" + /> + + {connected ? ( + + ) : ( + + )} + + {txWaitingConfrim === false && ( +
+ + Transaction pending confirmation +
+ )} + {txWaitingConfrim === true && ( +
+ + Transaction confirmed +
+ )} + + {/* {txWaitingConfrim === false && ( + + )} + + {txWaitingConfrim === true && ( + + )} */} + + + +
+ + ); +} diff --git a/packages/demo/components/pages/starterTemplates/data.ts b/packages/demo/components/pages/starterTemplates/data.ts index 1cfd4e09..030f613c 100644 --- a/packages/demo/components/pages/starterTemplates/data.ts +++ b/packages/demo/components/pages/starterTemplates/data.ts @@ -24,6 +24,12 @@ export const templates = { // image: 'marketplace.png', // comingsoon: true, // }, + signin: { + title: 'Sign In with Wallet', + desc: `Cryptographically prove the ownership of a wallet.`, + cli: 'signin', + image: 'signin.png', + }, }; export const languages = { diff --git a/packages/demo/components/pages/starterTemplates/index.tsx b/packages/demo/components/pages/starterTemplates/index.tsx index 4342bb84..739929c2 100644 --- a/packages/demo/components/pages/starterTemplates/index.tsx +++ b/packages/demo/components/pages/starterTemplates/index.tsx @@ -128,6 +128,34 @@ const items = [

), }, + { + title: 'Sign In Next.js TypeScript', + template: 'signin', + framework: 'next', + language: 'typescript', + installCode: 'npx create-mesh-app leap -t signin -s next -l ts', + demoUrl: 'http://signin-template.meshjs.dev/', + repoUrl: 'https://github.com/MeshJS/signin-next-ts-template', + desc: ( +

+ Cryptographically prove the ownership of a wallet by signing a piece of data using data sign. +

+ ), + }, + { + title: 'Sign In Next.js JavaScript', + template: 'signin', + framework: 'next', + language: 'javascript', + installCode: 'npx create-mesh-app leap -t signin -s next -l js', + demoUrl: 'http://signin-template.meshjs.dev/', + repoUrl: 'https://github.com/MeshJS/signin-next-js-template', + desc: ( +

+ Cryptographically prove the ownership of a wallet by signing a piece of data using data sign. +

+ ), + }, ]; export default function ReactStarterTemplates() { diff --git a/packages/demo/package.json b/packages/demo/package.json index 6f4b6b8c..3163db23 100644 --- a/packages/demo/package.json +++ b/packages/demo/package.json @@ -14,8 +14,8 @@ "dependencies": { "@headlessui/react": "^1.6.6", "@heroicons/react": "^2.0.10", - "@meshsdk/core": "1.3.0", - "@meshsdk/react": "1.1.3", + "@meshsdk/core": "1.4.0", + "@meshsdk/react": "1.1.4", "copy-to-clipboard": "^3.3.2", "flowbite": "^1.5.3", "flowbite-react": "^0.1.10", diff --git a/packages/demo/pages/providers/blockfrost.tsx b/packages/demo/pages/providers/blockfrost.tsx index 4b8ec400..159c15e3 100644 --- a/packages/demo/pages/providers/blockfrost.tsx +++ b/packages/demo/pages/providers/blockfrost.tsx @@ -4,22 +4,27 @@ import Metatags from '../../components/site/metatags'; import Codeblock from '../../components/ui/codeblock'; import { BadgeFetcher, + BadgeListener, BadgeSubmitter, } from '../../components/pages/providers/badges'; import Fetcher from '../../components/pages/providers/fetcher'; import { BlockfrostProvider } from '@meshsdk/core'; import Submitter from '../../components/pages/providers/submitter'; import ButtonGroup from '../../components/ui/buttongroup'; +import Listener from '../../components/pages/providers/listener'; export default function ProvidersBlockfrost() { const sidebarItems = [ { label: 'Fetch Account Info', to: 'fetchAccountInfo' }, + { label: 'Fetch Address Utxos', to: 'fetchAddressUtxos' }, { label: 'Fetch Asset Addresses', to: 'fetchAssetAddresses' }, { label: 'Fetch Asset Metadata', to: 'fetchAssetMetadata' }, - { label: 'Fetch Address Utxos', to: 'fetchAddressUtxos' }, + { label: 'Fetch Block Info', to: 'fetchBlockInfo' }, { label: 'Fetch Handle Address', to: 'fetchHandleAddress' }, { label: 'Fetch Protocol Parameters', to: 'fetchProtocolParameters' }, + { label: 'Fetch Transaction Info', to: 'fetchTxInfo' }, { label: 'Submit Tx', to: 'submitTx' }, + { label: 'On Transaction Confirmed', to: 'onTxConfirmed' }, ]; const [network, setNetwork] = useState('preprod'); @@ -38,7 +43,9 @@ export default function ProvidersBlockfrost() { } function Hero({ network, setNetwork }) { - let code1 = `import { BlockfrostProvider } from '@meshsdk/core';\n\nconst blockfrostProvider = new BlockfrostProvider('');\n`; + let code1 = `const blockfrostProvider = new BlockfrostProvider('');\n`; + let code2 = `const blockfrostProvider = new BlockfrostProvider('');\n`; + return (

@@ -46,6 +53,7 @@ function Hero({ network, setNetwork }) { +

@@ -59,10 +67,14 @@ function Hero({ network, setNetwork }) { Blockfrost {' '} provides restful APIs which allows your app to access information - stored on the blockchain. + stored on the blockchain. Get started:

-

Get started:

+

+ If you are using a privately hosted Blockfrost instance, you can set + the URL in the parameter: +

+

Choose network for this demo:

+ ); } diff --git a/packages/demo/pages/providers/index.tsx b/packages/demo/pages/providers/index.tsx index 38a4ac22..f68c240b 100644 --- a/packages/demo/pages/providers/index.tsx +++ b/packages/demo/pages/providers/index.tsx @@ -19,7 +19,11 @@ const ProvidersPage: NextPage = () => { link: '/providers/koios', thumbnail: '/providers/koios.png', }, - + { + title: 'Ogmios', + link: '/providers/ogmios', + thumbnail: '/providers/ogmios.png', + }, ]; return ( diff --git a/packages/demo/pages/providers/koios.tsx b/packages/demo/pages/providers/koios.tsx index 3e5e44b9..ad430780 100644 --- a/packages/demo/pages/providers/koios.tsx +++ b/packages/demo/pages/providers/koios.tsx @@ -5,21 +5,26 @@ import Codeblock from '../../components/ui/codeblock'; import { BadgeFetcher, BadgeSubmitter, + BadgeListener, } from '../../components/pages/providers/badges'; import Fetcher from '../../components/pages/providers/fetcher'; import { KoiosProvider } from '@meshsdk/core'; import Submitter from '../../components/pages/providers/submitter'; import ButtonGroup from '../../components/ui/buttongroup'; +import Listener from '../../components/pages/providers/listener'; export default function ProvidersKoios() { const sidebarItems = [ { label: 'Fetch Account Info', to: 'fetchAccountInfo' }, + { label: 'Fetch Address Utxos', to: 'fetchAddressUtxos' }, { label: 'Fetch Asset Addresses', to: 'fetchAssetAddresses' }, { label: 'Fetch Asset Metadata', to: 'fetchAssetMetadata' }, - { label: 'Fetch Address Utxos', to: 'fetchAddressUtxos' }, + { label: 'Fetch Block Info', to: 'fetchBlockInfo' }, { label: 'Fetch Handle Address', to: 'fetchHandleAddress' }, { label: 'Fetch Protocol Parameters', to: 'fetchProtocolParameters' }, + { label: 'Fetch Transaction Info', to: 'fetchTxInfo' }, { label: 'Submit Tx', to: 'submitTx' }, + { label: 'On Transaction Confirmed', to: 'onTxConfirmed' }, ]; const [network, setNetwork] = useState('preprod'); @@ -39,7 +44,8 @@ export default function ProvidersKoios() { } function Hero({ network, setNetwork }) { - let code1 = `import { KoiosProvider } from '@meshsdk/core';\n\nconst koiosProvider = new KoiosProvider('');\n`; + let code1 = `const koiosProvider = new KoiosProvider('');\n`; + let code2 = `const koiosProvider = new KoiosProvider('');\n`; return (
@@ -48,6 +54,7 @@ function Hero({ network, setNetwork }) { +

@@ -73,6 +80,11 @@ function Hero({ network, setNetwork }) { >

Get started:

+

+ If you are using a privately hosted instance, you can set the URL in + the parameter: +

+

Choose network for this demo:

setNetwork('preprod'), }, + { + key: 'preview', + label: 'Preview', + onClick: () => setNetwork('preview'), + }, + { + key: 'guild', + label: 'Guild', + onClick: () => setNetwork('guild'), + }, ]} currentSelected={network} /> @@ -112,6 +134,7 @@ function Main({ network }) { <> + ); } diff --git a/packages/demo/pages/providers/ogmios.tsx b/packages/demo/pages/providers/ogmios.tsx new file mode 100644 index 00000000..26898ec0 --- /dev/null +++ b/packages/demo/pages/providers/ogmios.tsx @@ -0,0 +1,110 @@ +import { useState, useEffect } from 'react'; +import CommonLayout from '../../components/common/layout'; +import Metatags from '../../components/site/metatags'; +import Codeblock from '../../components/ui/codeblock'; +import { + BadgeEvaluator, + BadgeSubmitter, +} from '../../components/pages/providers/badges'; +import { OgmiosProvider } from '@meshsdk/core'; +import Submitter from '../../components/pages/providers/submitter'; +import Evaluator from '../../components/pages/providers/evaluator'; +import ButtonGroup from '../../components/ui/buttongroup'; + +export default function ProvidersOgmios() { + const sidebarItems = [ + { label: 'Evaluate Tx', to: 'evaluateTx' }, + { label: 'Submit Tx', to: 'submitTx' }, + ]; + const [network, setNetwork] = useState('preprod'); + + return ( + <> + + + +
+ + + ); +} + +function Hero({ network, setNetwork }) { + let code1 = `const ogmiosProvider = new OgmiosProvider();\n`; + + return ( +
+

+ Ogmios + + + + +

+

+ Ogmios is a lightweight bridge interface for cardano-node. It offers a + WebSockets API that enables local clients to speak Ouroboros' + mini-protocols via JSON/RPC. +

+ +
+
+

+ Ogmios is a lightweight bridge interface for cardano-node. It offers + a WebSockets API that enables local clients to speak Ouroboros' + mini-protocols via JSON/RPC. Ogmios is a fast and lightweight + solution that can be deployed alongside relays to create entry + points on the Cardano network for various types of applications. + (reference:{' '} + + ogmios.dev + + ) +

+

Get started:

+ + +

Choose network for this demo:

+ setNetwork('mainnet'), + }, + { + key: 'preprod', + label: 'Preprod', + onClick: () => setNetwork('preprod'), + }, + ]} + currentSelected={network} + /> +
+
+
+
+ ); +} + +function Main({ network }) { + const [provider, setProvider] = useState(null); + + useEffect(() => { + async function load() { + const _provider = new OgmiosProvider('preprod'); + setProvider(_provider); + } + load(); + }, [network]); + + return ( + <> + + + + ); +} diff --git a/packages/demo/pages/providers/tangocrypto.tsx b/packages/demo/pages/providers/tangocrypto.tsx index 90423df4..83d07880 100644 --- a/packages/demo/pages/providers/tangocrypto.tsx +++ b/packages/demo/pages/providers/tangocrypto.tsx @@ -5,21 +5,29 @@ import Codeblock from '../../components/ui/codeblock'; import { BadgeFetcher, BadgeSubmitter, + BadgeListener, + BadgeEvaluator, } from '../../components/pages/providers/badges'; import Fetcher from '../../components/pages/providers/fetcher'; import { TangoProvider } from '@meshsdk/core'; import Submitter from '../../components/pages/providers/submitter'; import ButtonGroup from '../../components/ui/buttongroup'; +import Listener from '../../components/pages/providers/listener'; +import Evaluator from '../../components/pages/providers/evaluator'; export default function ProvidersTangocrypto() { const sidebarItems = [ { label: 'Fetch Account Info', to: 'fetchAccountInfo' }, + { label: 'Fetch Address Utxos', to: 'fetchAddressUtxos' }, { label: 'Fetch Asset Addresses', to: 'fetchAssetAddresses' }, { label: 'Fetch Asset Metadata', to: 'fetchAssetMetadata' }, - { label: 'Fetch Address Utxos', to: 'fetchAddressUtxos' }, + { label: 'Fetch Block Info', to: 'fetchBlockInfo' }, { label: 'Fetch Handle Address', to: 'fetchHandleAddress' }, { label: 'Fetch Protocol Parameters', to: 'fetchProtocolParameters' }, + { label: 'Fetch Transaction Info', to: 'fetchTxInfo' }, { label: 'Submit Tx', to: 'submitTx' }, + { label: 'On Transaction Confirmed', to: 'onTxConfirmed' }, + { label: 'Evaluate Tx', to: 'evaluateTx' }, ]; const [network, setNetwork] = useState('preprod'); @@ -40,7 +48,7 @@ export default function ProvidersTangocrypto() { function Hero({ network, setNetwork }) { let codeTango = `import { TangoProvider } from '@meshsdk/core';\n\n`; codeTango += `const tangocryptoProvider = new TangoProvider(\n`; - codeTango += ` '',\n`; + codeTango += ` '',\n`; codeTango += ` ''\n`; codeTango += ` ''\n`; codeTango += `);`; @@ -52,6 +60,8 @@ function Hero({ network, setNetwork }) { + +

@@ -128,6 +138,14 @@ function Main({ network }) { submitter={tangocryptoProvider} submitterName="tangocryptoProvider" /> + + ); } diff --git a/packages/demo/public/providers/ogmios.png b/packages/demo/public/providers/ogmios.png new file mode 100644 index 00000000..d1cac84c Binary files /dev/null and b/packages/demo/public/providers/ogmios.png differ diff --git a/packages/demo/public/templates/signin.png b/packages/demo/public/templates/signin.png new file mode 100644 index 00000000..b24b819a Binary files /dev/null and b/packages/demo/public/templates/signin.png differ diff --git a/packages/module/package.json b/packages/module/package.json index 1e93bd3d..85557dae 100644 --- a/packages/module/package.json +++ b/packages/module/package.json @@ -3,7 +3,7 @@ "description": "Rapidly build Web3 apps on the Cardano Blockchain.", "homepage": "https://meshjs.dev", "author": "MeshJS", - "version": "1.3.0", + "version": "1.4.0", "license": "Apache-2.0", "type": "module", "repository": { diff --git a/packages/module/src/common/constants.ts b/packages/module/src/common/constants.ts index b7730ab4..93e50ce5 100644 --- a/packages/module/src/common/constants.ts +++ b/packages/module/src/common/constants.ts @@ -80,6 +80,13 @@ export const SUPPORTED_HANDLES: Record = { [csl.NetworkInfo.mainnet().network_id()]: 'f0ff48bbb7bbe9d59a40f1ce90e9e9d0ff5002ec48f232b49ca0fb9a', }; +export const SUPPORTED_OGMIOS_LINKS: Record = { + mainnet: 'wss://ogmios-api.mainnet.dandelion.link', + preprod: 'wss://ogmios-api.iohk-preprod.dandelion.link', + preview: '__TBD_SOON_TM__', + testnet: 'wss://ogmios-api.testnet.dandelion.link', +}; + export const SUPPORTED_WALLETS = [ 'begin', 'eternl', 'flint', 'nami', 'nufi', 'gerowallet', 'typhoncip30', ]; diff --git a/packages/module/src/common/contracts/evaluator.ts b/packages/module/src/common/contracts/evaluator.ts new file mode 100644 index 00000000..843a2ecd --- /dev/null +++ b/packages/module/src/common/contracts/evaluator.ts @@ -0,0 +1,5 @@ +import { Action } from '@mesh/common/types'; + +export interface IEvaluator { + evaluateTx(tx: string): Promise[]>; +} diff --git a/packages/module/src/common/contracts/fetcher.ts b/packages/module/src/common/contracts/fetcher.ts index b9ee1126..7d13d090 100644 --- a/packages/module/src/common/contracts/fetcher.ts +++ b/packages/module/src/common/contracts/fetcher.ts @@ -1,5 +1,6 @@ import type { - AccountInfo, AssetMetadata, Protocol, UTxO, + AccountInfo, AssetMetadata, BlockInfo, + Protocol, UTxO, TransactionInfo, } from '@mesh/common/types'; export interface IFetcher { @@ -9,6 +10,8 @@ export interface IFetcher { asset: string, ): Promise<{ address: string; quantity: string }[]>; fetchAssetMetadata(asset: string): Promise; + fetchBlockInfo(hash: string): Promise; fetchHandleAddress(handle: string): Promise; fetchProtocolParameters(epoch: number): Promise; + fetchTxInfo(hash: string): Promise; } diff --git a/packages/module/src/common/contracts/index.ts b/packages/module/src/common/contracts/index.ts index 62b48363..499124d7 100644 --- a/packages/module/src/common/contracts/index.ts +++ b/packages/module/src/common/contracts/index.ts @@ -1,5 +1,7 @@ +export * from './evaluator'; export * from './fetcher'; export * from './initiator'; +export * from './listener'; export * from './signer'; export * from './submitter'; export * from './uploader'; diff --git a/packages/module/src/common/contracts/listener.ts b/packages/module/src/common/contracts/listener.ts new file mode 100644 index 00000000..96b3bbc5 --- /dev/null +++ b/packages/module/src/common/contracts/listener.ts @@ -0,0 +1,3 @@ +export interface IListener { + onTxConfirmed(txHash: string, callback: () => void, limit?: number): void; +} diff --git a/packages/module/src/common/helpers/index.ts b/packages/module/src/common/helpers/index.ts index 1711e50e..c71746fe 100644 --- a/packages/module/src/common/helpers/index.ts +++ b/packages/module/src/common/helpers/index.ts @@ -1,2 +1,4 @@ export * from './generateNonce'; export * from './mergeSignatures'; +export * from './readPlutusData'; +export * from './readTransaction'; diff --git a/packages/module/src/common/helpers/readTransaction.ts b/packages/module/src/common/helpers/readTransaction.ts new file mode 100644 index 00000000..71a1e00f --- /dev/null +++ b/packages/module/src/common/helpers/readTransaction.ts @@ -0,0 +1,6 @@ +import { csl } from '@mesh/core'; +import { deserializeTx } from '@mesh/common/utils'; + +export const readTransaction = (tx: string): csl.TransactionJSON => { + return deserializeTx(tx).to_js_value(); +}; diff --git a/packages/module/src/common/types/BlockInfo.ts b/packages/module/src/common/types/BlockInfo.ts new file mode 100644 index 00000000..0f27c1fd --- /dev/null +++ b/packages/module/src/common/types/BlockInfo.ts @@ -0,0 +1,17 @@ +export type BlockInfo = { + time: number; + hash: string; + slot: string; + epoch: number; + epochSlot: string; + slotLeader: string; + size: number; + txCount: number; + output: string; + fees: string; + previousBlock: string; + nextBlock: string; + confirmations: number; + operationalCertificate: string; + VRFKey: string; +}; diff --git a/packages/module/src/common/types/Network.ts b/packages/module/src/common/types/Network.ts index 2660efa8..7b8b7ebb 100644 --- a/packages/module/src/common/types/Network.ts +++ b/packages/module/src/common/types/Network.ts @@ -1,5 +1,7 @@ -export type Network = -| 'testnet' -| 'preview' -| 'preprod' -| 'mainnet'; +const ALL_NETWORKS = ['testnet', 'preview', 'preprod', 'mainnet'] as const; + +export type Network = typeof ALL_NETWORKS[number]; + +export const isNetwork = (value: unknown): value is Network => { + return ALL_NETWORKS.includes(value as Network); +}; diff --git a/packages/module/src/common/types/PoolParams.ts b/packages/module/src/common/types/PoolParams.ts index 94986cf8..f9386f36 100644 --- a/packages/module/src/common/types/PoolParams.ts +++ b/packages/module/src/common/types/PoolParams.ts @@ -1,14 +1,14 @@ import { Relay } from './Relay'; export type PoolParams = { + VRFKeyHash: string; operator: string; - vrfKeyHash: string; pledge: string; cost: string; margin: number; - rewardAddress: string; relays: Relay[]; owners: string[]; + rewardAddress: string; metadata?: PoolMetadata; }; diff --git a/packages/module/src/common/types/TransactionInfo.ts b/packages/module/src/common/types/TransactionInfo.ts new file mode 100644 index 00000000..8dac44cd --- /dev/null +++ b/packages/module/src/common/types/TransactionInfo.ts @@ -0,0 +1,11 @@ +export type TransactionInfo = { + index: number; + block: string; + hash: string; + slot: string; + fees: string; + size: number; + deposit: string; + invalidBefore: string; + invalidAfter: string; +}; diff --git a/packages/module/src/common/types/index.ts b/packages/module/src/common/types/index.ts index 58fb0fe4..b9408019 100644 --- a/packages/module/src/common/types/index.ts +++ b/packages/module/src/common/types/index.ts @@ -4,6 +4,7 @@ export * from './Action'; export * from './Asset'; export * from './AssetExtended'; export * from './AssetMetadata'; +export * from './BlockInfo'; export * from './Data'; export * from './DataSignature'; export * from './Era'; @@ -15,5 +16,6 @@ export * from './PoolParams'; export * from './Protocol'; export * from './Recipient'; export * from './Relay'; +export * from './TransactionInfo'; export * from './UTxO'; export * from './Wallet'; diff --git a/packages/module/src/providers/blockfrost.provider.ts b/packages/module/src/providers/blockfrost.provider.ts index b9032059..f0032ff4 100644 --- a/packages/module/src/providers/blockfrost.provider.ts +++ b/packages/module/src/providers/blockfrost.provider.ts @@ -1,24 +1,32 @@ import axios, { AxiosInstance } from 'axios'; import { SUPPORTED_HANDLES } from '@mesh/common/constants'; -import { IFetcher, ISubmitter } from '@mesh/common/contracts'; +import { IFetcher, IListener, ISubmitter } from '@mesh/common/contracts'; import { fromUTF8, parseAssetUnit, parseHttpError, resolveRewardAddress, toBytes, toScriptRef, } from '@mesh/common/utils'; import type { - AccountInfo, AssetMetadata, NativeScript, - PlutusScript, Protocol, UTxO, + AccountInfo, AssetMetadata, BlockInfo, NativeScript, + PlutusScript, Protocol, TransactionInfo, UTxO, } from '@mesh/common/types'; -export class BlockfrostProvider implements IFetcher, ISubmitter { +export class BlockfrostProvider implements IFetcher, IListener, ISubmitter { private readonly _axiosInstance: AxiosInstance; - constructor(projectId: string, version = 0) { - const network = projectId.slice(0, 7); - this._axiosInstance = axios.create({ - baseURL: `https://cardano-${network}.blockfrost.io/api/v${version}`, - headers: { project_id: projectId }, - }); + constructor(baseUrl: string); + constructor(projectId: string, version?: number); + + constructor(...args: unknown[]) { + if (typeof args[0] === 'string' && args[0].startsWith('http')) { + this._axiosInstance = axios.create({ baseURL: args[0] }); + } else { + const projectId = args[0] as string; + const network = projectId.slice(0, 7); + this._axiosInstance = axios.create({ + baseURL: `https://cardano-${network}.blockfrost.io/api/v${args[1] ?? 0}`, + headers: { project_id: projectId }, + }); + } } async fetchAccountInfo(address: string): Promise { @@ -147,6 +155,37 @@ export class BlockfrostProvider implements IFetcher, ISubmitter { } } + async fetchBlockInfo(hash: string): Promise { + try { + const { data, status } = await this._axiosInstance.get( + `blocks/${hash}`, + ); + + if (status === 200) + return { + confirmations: data.confirmations, + epoch: data.epoch, + epochSlot: data.epoch_slot.toString(), + fees: data.fees, + hash: data.hash, + nextBlock: data.next_block ?? '', + operationalCertificate: data.op_cert, + output: data.output ?? '0', + previousBlock: data.previous_block, + size: data.size, + slot: data.slot.toString(), + slotLeader: data.slot_leader ?? '', + time: data.time, + txCount: data.tx_count, + VRFKey: data.block_vrf, + }; + + throw parseHttpError(data); + } catch (error) { + throw parseHttpError(error); + } + } + async fetchHandleAddress(handle: string): Promise { try { const assetName = fromUTF8(handle.replace('$', '')); @@ -199,6 +238,49 @@ export class BlockfrostProvider implements IFetcher, ISubmitter { } } + async fetchTxInfo(hash: string): Promise { + try { + const { data, status } = await this._axiosInstance.get( + `txs/${hash}`, + ); + + if (status === 200) + return { + block: data.block, + deposit: data.deposit, + fees: data.fees, + hash: data.hash, + index: data.index, + invalidAfter: data.invalid_hereafter ?? '', + invalidBefore: data.invalid_before ?? '', + slot: data.slot.toString(), + size: data.size, + }; + + throw parseHttpError(data); + } catch (error) { + throw parseHttpError(error); + } + } + + onTxConfirmed(txHash: string, callback: () => void, limit = 20): void { + let attempts = 0; + + const checkTx = setInterval(() => { + if (attempts >= limit) + clearInterval(checkTx); + + this.fetchTxInfo(txHash).then((txInfo) => { + this.fetchBlockInfo(txInfo.block).then((blockInfo) => { + if (blockInfo?.confirmations > 0) { + clearInterval(checkTx); + callback(); + } + }).catch(() => { attempts += 1; }); + }).catch(() => { attempts += 1; }); + }, 5_000); + } + async submitTx(tx: string): Promise { try { const headers = { 'Content-Type': 'application/cbor' }; diff --git a/packages/module/src/providers/index.ts b/packages/module/src/providers/index.ts index f997f6ed..d8bafb83 100644 --- a/packages/module/src/providers/index.ts +++ b/packages/module/src/providers/index.ts @@ -1,4 +1,5 @@ export * from './blockfrost.provider'; export * from './infura.provider'; export * from './koios.provider'; +export * from './ogmios.provider'; export * from './tango.provider'; diff --git a/packages/module/src/providers/koios.provider.ts b/packages/module/src/providers/koios.provider.ts index 23ac5153..d3bf0e4b 100644 --- a/packages/module/src/providers/koios.provider.ts +++ b/packages/module/src/providers/koios.provider.ts @@ -1,23 +1,31 @@ import axios, { AxiosInstance } from 'axios'; import { SUPPORTED_HANDLES } from '@mesh/common/constants'; -import { IFetcher, ISubmitter } from '@mesh/common/contracts'; +import { IFetcher, IListener, ISubmitter } from '@mesh/common/contracts'; import { deserializeNativeScript, fromNativeScript, fromUTF8, parseAssetUnit, parseHttpError, resolveRewardAddress, toBytes, toScriptRef, toUTF8, } from '@mesh/common/utils'; import type { - AccountInfo, Asset, AssetMetadata, - PlutusScript, Protocol, UTxO, + AccountInfo, Asset, AssetMetadata, BlockInfo, + PlutusScript, Protocol, TransactionInfo, UTxO, } from '@mesh/common/types'; -export class KoiosProvider implements IFetcher, ISubmitter { +export class KoiosProvider implements IFetcher, IListener, ISubmitter { private readonly _axiosInstance: AxiosInstance; - constructor(network: 'api' | 'preview' | 'preprod' | 'guild', version = 0) { - this._axiosInstance = axios.create({ - baseURL: `https://${network}.koios.rest/api/v${version}`, - }); + constructor(baseUrl: string); + constructor(network: 'api' | 'preview' | 'preprod' | 'guild', version?: number); + + constructor(...args: unknown[]) { + if (typeof args[0] === 'string' && args[0].startsWith('http')) { + this._axiosInstance = axios.create({ baseURL: args[0] }); + } else { + this._axiosInstance = axios.create({ + baseURL: `https://${args[0]}.koios.rest/api/v${args[1] ?? 0}`, + }); + } + } async fetchAccountInfo(address: string): Promise { @@ -143,6 +151,37 @@ export class KoiosProvider implements IFetcher, ISubmitter { } } + async fetchBlockInfo(hash: string): Promise { + try { + const { data, status } = await this._axiosInstance.post( + 'block_info', { _block_hashes: [hash] } + ); + + if (status === 200) + return { + confirmations: data[0].num_confirmations, + epoch: data[0].epoch_no, + epochSlot: data[0].epoch_slot.toString(), + fees: data[0].total_fees ?? '', + hash: data[0].hash, + nextBlock: data[0].child_hash ?? '', + operationalCertificate: data[0].op_cert, + output: data[0].total_output ?? '0', + previousBlock: data[0].parent_hash, + size: data[0].block_size, + slot: data[0].abs_slot.toString(), + slotLeader: data[0].pool ?? '', + time: data[0].block_time, + txCount: data[0].tx_count, + VRFKey: data[0].vrf_key, + }; + + throw parseHttpError(data); + } catch (error) { + throw parseHttpError(error); + } + } + async fetchHandleAddress(handle: string): Promise { try { const assetName = fromUTF8(handle.replace('$', '')); @@ -195,6 +234,49 @@ export class KoiosProvider implements IFetcher, ISubmitter { } } + async fetchTxInfo(hash: string): Promise { + try { + const { data, status } = await this._axiosInstance.post( + 'tx_info', { _tx_hashes: [hash] }, + ); + + if (status === 200) + return { + block: data[0].block_hash, + deposit: data[0].deposit, + fees: data[0].fee, + hash: data[0].tx_hash, + index: data[0].tx_block_index, + invalidAfter: data[0].invalid_after?.toString() ?? '', + invalidBefore: data[0].invalid_before?.toString() ?? '', + slot: data[0].absolute_slot.toString(), + size: data[0].tx_size, + }; + + throw parseHttpError(data); + } catch (error) { + throw parseHttpError(error); + } + } + + onTxConfirmed(txHash: string, callback: () => void, limit = 20): void { + let attempts = 0; + + const checkTx = setInterval(() => { + if (attempts >= limit) + clearInterval(checkTx); + + this.fetchTxInfo(txHash).then((txInfo) => { + this.fetchBlockInfo(txInfo.block).then((blockInfo) => { + if (blockInfo?.confirmations > 0) { + clearInterval(checkTx); + callback(); + } + }).catch(() => { attempts += 1; }); + }).catch(() => { attempts += 1; }); + }, 5_000); + } + async submitTx(tx: string): Promise { try { const headers = { 'Content-Type': 'application/cbor' }; diff --git a/packages/module/src/providers/ogmios.provider.ts b/packages/module/src/providers/ogmios.provider.ts new file mode 100644 index 00000000..9cb2ca4c --- /dev/null +++ b/packages/module/src/providers/ogmios.provider.ts @@ -0,0 +1,122 @@ +import { SUPPORTED_OGMIOS_LINKS } from '@mesh/common/constants'; +import { IEvaluator, ISubmitter } from '@mesh/common/contracts'; +import { Action, isNetwork, Network } from '@mesh/common/types'; + +export class OgmiosProvider implements IEvaluator, ISubmitter { + private readonly _baseUrl: string; + + constructor(baseUrl: string); + constructor(network: Network); + + constructor(...args: unknown[]) { + this._baseUrl = isNetwork(args[0]) + ? SUPPORTED_OGMIOS_LINKS[args[0]] + : args[0] as string; + } + + async evaluateTx(tx: string): Promise[]> { + const client = await this.open(); + + this.send(client, 'EvaluateTx', { + evaluate: tx, + }); + + return new Promise((resolve, reject) => { + client.addEventListener('message', (response: MessageEvent) => { + try { + const { result } = JSON.parse(response.data); + + if (result.EvaluationResult) { + resolve( + Object.keys(result.EvaluationResult).map((key) => + >{ + index: parseInt(key.split(':')[1], 10), + tag: key.split(':')[0].toUpperCase(), + budget: { + mem: result.EvaluationResult[key].memory, + steps: result.EvaluationResult[key].steps, + }, + } + ) + ); + } else { + reject(result.EvaluationFailure); + } + + client.close(); + } catch (error) { + reject(error); + } + }, { once: true }); + }); + } + + async onNextTx(callback: (tx: unknown) => void): Promise<() => void> { + const client = await this.open(); + + this.send(client, 'AwaitAcquire', {}); + + client.addEventListener('message', (response: MessageEvent) => { + const { result } = JSON.parse(response.data); + + if (result === null) { + return this.send(client, 'AwaitAcquire', {}); + } + + if (result.AwaitAcquired === undefined) { + callback(result); + } + + this.send(client, 'NextTx', {}); + }); + + return () => client.close(); + } + + async submitTx(tx: string): Promise { + const client = await this.open(); + + this.send(client, 'SubmitTx', { + submit: tx, + }); + + return new Promise((resolve, reject) => { + client.addEventListener('message', (response: MessageEvent) => { + try { + const { result } = JSON.parse(response.data); + + if (result.SubmitSuccess) { + resolve(result.SubmitSuccess.txId); + } else { + reject(result.SubmitFail); + } + + client.close(); + } catch (error) { + reject(error); + } + }, { once: true }); + }); + } + + private async open(): Promise { + const client = new WebSocket(this._baseUrl); + + await new Promise((resolve) => { + client.addEventListener('open', () => resolve(true), { once: true }); + }); + + return client; + } + + private send(client: WebSocket, methodname: string, args: unknown) { + client.send( + JSON.stringify({ + version: '1.0', + type: 'jsonwsp/request', + servicename: 'ogmios', + methodname, args, + }) + ); + } +} diff --git a/packages/module/src/providers/tango.provider.ts b/packages/module/src/providers/tango.provider.ts index 6828d117..b83227e5 100644 --- a/packages/module/src/providers/tango.provider.ts +++ b/packages/module/src/providers/tango.provider.ts @@ -1,17 +1,17 @@ import axios, { AxiosInstance } from 'axios'; import { SUPPORTED_HANDLES } from '@mesh/common/constants'; -import { IFetcher, ISubmitter } from '@mesh/common/contracts'; +import { IEvaluator, IFetcher, IListener, ISubmitter } from '@mesh/common/contracts'; import { deserializeNativeScript, fromNativeScript, fromUTF8, parseAssetUnit, parseHttpError, resolveRewardAddress, toScriptRef, toUTF8, } from '@mesh/common/utils'; import type { - AccountInfo, Asset, AssetMetadata, - PlutusScript, Protocol, UTxO, + AccountInfo, Action, Asset, AssetMetadata, BlockInfo, + PlutusScript, Protocol, TransactionInfo, UTxO, } from '@mesh/common/types'; -export class TangoProvider implements IFetcher, ISubmitter { +export class TangoProvider implements IEvaluator, IFetcher, IListener, ISubmitter { private readonly _axiosInstance: AxiosInstance; constructor( @@ -25,6 +25,28 @@ export class TangoProvider implements IFetcher, ISubmitter { }); } + async evaluateTx(tx: string): Promise[]> { + try { + const { data, status } = await this._axiosInstance.post( + 'transactions/evaluate', { tx, utxos: [] }, + ); + + if (status === 200) + return data.redeemers.map((redeemer) => >({ + index: redeemer.index, + tag: redeemer.purpose.toUpperCase(), + budget: { + mem: redeemer.unit_mem, + steps: redeemer.unit_steps, + }, + })); + + throw parseHttpError(data); + } catch (error) { + throw parseHttpError(error); + } + } + async fetchAccountInfo(address: string): Promise { try { const rewardAddress = address.startsWith('addr') @@ -157,7 +179,38 @@ export class TangoProvider implements IFetcher, ISubmitter { if (status === 200) return { - ...data.metadata[0]?.json[policyId][toUTF8(assetName)], + ...data.metadata.find((m) => m.label === 721)?.json[policyId][toUTF8(assetName)], + }; + + throw parseHttpError(data); + } catch (error) { + throw parseHttpError(error); + } + } + + async fetchBlockInfo(hash: string): Promise { + try { + const { data, status } = await this._axiosInstance.get( + `blocks/${hash}`, + ); + + if (status === 200) + return { + confirmations: data.confirmations, + epoch: data.epoch_no, + epochSlot: data.epoch_slot_no.toString(), + fees: data.fees.toString(), + hash: data.hash, + nextBlock: data.next_block.toString() ?? '', + operationalCertificate: data.op_cert, + output: data.out_sum.toString() ?? '0', + previousBlock: data.previous_block.toString(), + size: data.size, + slot: data.slot_no.toString(), + slotLeader: data.slot_leader ?? '', + time: Date.parse(data.time), + txCount: data.tx_count, + VRFKey: data.vrf_key, }; throw parseHttpError(data); @@ -218,6 +271,49 @@ export class TangoProvider implements IFetcher, ISubmitter { } } + async fetchTxInfo(hash: string): Promise { + try { + const { data, status } = await this._axiosInstance.get( + `transactions/${hash}`, + ); + + if (status === 200) + return { + block: data.block.hash, + deposit: data.deposit, + fees: data.fee, + hash: data.hash, + index: data.block_index, + invalidAfter: data.invalid_hereafter ?? '', + invalidBefore: data.invalid_before ?? '', + slot: data.block.slot_no.toString(), + size: data.size, + }; + + throw parseHttpError(data); + } catch (error) { + throw parseHttpError(error); + } + } + + onTxConfirmed(txHash: string, callback: () => void, limit = 20): void { + let attempts = 0; + + const checkTx = setInterval(() => { + if (attempts >= limit) + clearInterval(checkTx); + + this.fetchTxInfo(txHash).then((txInfo) => { + this.fetchBlockInfo(txInfo.block).then((blockInfo) => { + if (blockInfo?.confirmations > 0) { + clearInterval(checkTx); + callback(); + } + }).catch(() => { attempts += 1; }); + }).catch(() => { attempts += 1; }); + }, 5_000); + } + async submitTx(tx: string): Promise { try { const headers = { 'Content-Type': 'application/json' }; diff --git a/packages/module/src/transaction/transaction.service.ts b/packages/module/src/transaction/transaction.service.ts index 074b981d..02b00e7c 100644 --- a/packages/module/src/transaction/transaction.service.ts +++ b/packages/module/src/transaction/transaction.service.ts @@ -49,7 +49,7 @@ export class Transaction { this._txWithdrawals = csl.Withdrawals.new(); } - static maskMetadata(cborTx: string, era: Era = 'ALONZO') { + static maskMetadata(cborTx: string, era: Era = 'BABBAGE') { const tx = deserializeTx(cborTx); const txMetadata = tx.auxiliary_data()?.metadata(); @@ -88,7 +88,7 @@ export class Transaction { return tx.auxiliary_data()?.metadata()?.to_hex() ?? ''; } - static writeMetadata(cborTx: string, cborTxMetadata: string, era: Era = 'ALONZO') { + static writeMetadata(cborTx: string, cborTxMetadata: string, era: Era = 'BABBAGE') { const tx = deserializeTx(cborTx); const txAuxData = tx.auxiliary_data() ?? csl.AuxiliaryData.new(); @@ -328,14 +328,18 @@ export class Transaction { if (amount.is_zero() || multiAsset === undefined) return this; - const txOutputBuilder = buildTxOutputBuilder( + const txOutputAmountBuilder = buildTxOutputBuilder( recipient, - ); + ).next(); - const txOutput = txOutputBuilder.next() - .with_asset_and_min_required_coin_by_utxo_cost(multiAsset, - buildDataCost(this._protocolParameters.coinsPerUTxOSize), - ).build(); + const txOutput = amount.coin().is_zero() + ? txOutputAmountBuilder + .with_asset_and_min_required_coin_by_utxo_cost(multiAsset, + buildDataCost(this._protocolParameters.coinsPerUTxOSize), + ).build() + : txOutputAmountBuilder + .with_coin_and_asset(amount.coin(), multiAsset) + .build(); this._txBuilder.add_output(txOutput); diff --git a/packages/react/package.json b/packages/react/package.json index 471d5434..fff973fe 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -3,7 +3,7 @@ "description": "React Hooks & Components you need for building dApps on Cardano.", "homepage": "https://meshjs.dev", "author": "MeshJS", - "version": "1.1.3", + "version": "1.1.4", "license": "Apache-2.0", "type": "module", "repository": { @@ -61,7 +61,7 @@ "vite": "3.1.4" }, "peerDependencies": { - "@meshsdk/core": "1.3.0", + "@meshsdk/core": "1.4.0", "react-dom": "17.x || 18.x", "react": "17.x || 18.x" },