diff --git a/package.json b/package.json index a75f7f7b..cf62dca6 100644 --- a/package.json +++ b/package.json @@ -13,11 +13,13 @@ "dependencies": { "@apollo/client": "^3.5.10", "@cardano-graphql/client-ts": "^7.0.0", - "@dcspark/cardano-multiplatform-lib-browser": "^0.3.0", - "@heroicons/react": "^2.0.11", + "@dcspark/cardano-multiplatform-lib-browser": "^3.1.0", + "@heroicons/react": "^2.0.12", "buffer": "^6.0.3", + "cardano-utxo-wasm": "^0.2.0", "dexie": "^3.2.1", "dexie-react-hooks": "^1.1.1", + "fractional": "^1.0.0", "graphql": "^16.6.0", "gun": "^0.2020.1238", "nanoid": "^4.0.0", diff --git a/src/cardano/multiplatform-lib.ts b/src/cardano/multiplatform-lib.ts index 993edb84..5e5a5f42 100644 --- a/src/cardano/multiplatform-lib.ts +++ b/src/cardano/multiplatform-lib.ts @@ -1,11 +1,14 @@ import type { ProtocolParams, TransactionOutput } from '@cardano-graphql/client-ts' -import type { Address, BaseAddress, BigNum, Ed25519KeyHash, NativeScript, NetworkInfo, ScriptHash, Transaction, TransactionBuilder, TransactionHash, TransactionOutput as CardanoTransactionOutput, TransactionUnspentOutputs, Value as CardanoValue, Vkeywitness } from '@dcspark/cardano-multiplatform-lib-browser' +import type { Address, BaseAddress, BigNum, Ed25519KeyHash, NativeScript, NetworkInfo, ScriptHash, SingleInputBuilder, SingleOutputBuilderResult, Transaction, TransactionBuilder, TransactionHash, Value as CMLValue, Vkeywitness } from '@dcspark/cardano-multiplatform-lib-browser' import { nanoid } from 'nanoid' import { useEffect, useState } from 'react' import type { Config } from './config' import type { Value } from './query-api' import { getAssetName, getPolicyId } from './query-api' +const Fraction = require('fractional').Fraction +type Fraction = { numerator: number, denominator: number } + type CardanoWASM = typeof import('@dcspark/cardano-multiplatform-lib-browser') type MultiSigType = 'all' | 'any' | 'atLeast' type Recipient = { @@ -62,7 +65,7 @@ function toIter(set: CardanoIterable): IterableIterator { value: set.get(index++) } : { done: true, value: null } }, - [Symbol.iterator]: function() { return this } + [Symbol.iterator]: function () { return this } } } @@ -101,29 +104,35 @@ class Cardano { return this._wasm } - public buildTxOutput(recipient: Recipient): Result { - const { Address, TransactionOutputBuilder } = this.lib - return getResult(() => { - const address = Address.from_bech32(recipient.address) - const builder = TransactionOutputBuilder - .new() - .with_address(address) - .next() - const value = this.getCardanoValue(recipient.value) - return builder.with_value(value).build() - }) + public buildTxOutput(recipient: Recipient, protocolParams: ProtocolParams): SingleOutputBuilderResult { + if (recipient.value.lovelace < this.getMinLovelace(recipient, protocolParams)) { + const error = new Error('Insufficient ADA') + error.name = 'InsufficientADAError' + throw error + } + const { TransactionOutputBuilder } = this.lib + return TransactionOutputBuilder + .new() + .with_address(this.parseAddress(recipient.address)) + .next() + .with_value(this.buildCMLValue(recipient.value)) + .build() } - public getMinLovelace(value: Value, hasDataHash: boolean, coinsPerUtxoByte: number): bigint { + public getMinLovelace(recipient: Recipient, protocolParams: ProtocolParams): bigint { + const { BigNum, TransactionOutput } = this.lib + if (!protocolParams.coinsPerUtxoByte) throw new Error('coinsPerUtxoByte is missing') + const coinsPerUtxoByte = BigNum.from_str(protocolParams.coinsPerUtxoByte.toString()) + const address = this.parseAddress(recipient.address) + const txOutput = TransactionOutput.new(address, this.buildCMLValue(recipient.value)) const minimum = this.lib.min_ada_required( - this.getCardanoValue(value), - hasDataHash, - this.lib.BigNum.from_str(coinsPerUtxoByte.toString()) + txOutput, + coinsPerUtxoByte ) return BigInt(minimum.to_str()) } - public getCardanoValue(value: Value): CardanoValue { + public buildCMLValue(value: Value): CMLValue { const { AssetName, BigNum, MultiAsset, ScriptHash } = this.lib const { lovelace, assets } = value const cardanoValue = this.lib.Value.new(BigNum.from_str(lovelace.toString())) @@ -140,6 +149,27 @@ class Cardano { return cardanoValue } + public createTxInputBuilder(input: TransactionOutput): SingleInputBuilder { + const { AssetName, BigNum, MultiAsset, ScriptHash, SingleInputBuilder, TransactionHash, TransactionInput, } = this.lib + const hash = TransactionHash.from_hex(input.txHash) + const index = BigNum.from_str(input.index.toString()) + const txInput = TransactionInput.new(hash, index) + const value = this.lib.Value.new(BigNum.from_str(input.value)) + if (input.tokens.length > 0) { + const multiAsset = MultiAsset.new() + input.tokens.forEach((token) => { + const assetId = token.asset.assetId + const policyId = ScriptHash.from_bytes(Buffer.from(getPolicyId(assetId), 'hex')) + const assetName = AssetName.new(Buffer.from(getAssetName(assetId), 'hex')) + const quantity = BigNum.from_str(token.quantity.toString()) + multiAsset.set_asset(policyId, assetName, quantity) + }) + value.set_multiasset(multiAsset) + } + const txOuput = this.lib.TransactionOutput.new(this.parseAddress(input.address), value) + return SingleInputBuilder.new(txInput, txOuput) + } + public getMessageLabel(): BigNum { return this.lib.BigNum.from_str('674') } @@ -168,29 +198,18 @@ class Cardano { return toHex(witnessSet) } - public parseAddress(bech32Address: string): Result
{ - return getResult(() => this.lib.Address.from_bech32(bech32Address)) + public parseAddress(address: string): Address { + const { Address, ByronAddress } = this.lib + if (Address.is_valid_bech32(address)) return Address.from_bech32(address) + if (Address.is_valid_byron(address)) return ByronAddress.from_base58(address).to_address() + const error = new Error('The address is invalid.') + error.name = 'InvalidAddressError' + throw error } - public chainCoinSelection(builder: TransactionBuilder, UTxOSet: TransactionUnspentOutputs, address: Address): void { - const Strategy = this.lib.CoinSelectionStrategyCIP2 - try { - builder.add_inputs_from(UTxOSet, Strategy.RandomImprove) - builder.add_change_if_needed(address) - } catch { - try { - builder.add_inputs_from(UTxOSet, Strategy.LargestFirst) - builder.add_change_if_needed(address) - } catch { - try { - builder.add_inputs_from(UTxOSet, Strategy.RandomImproveMultiAsset) - builder.add_change_if_needed(address) - } catch { - builder.add_inputs_from(UTxOSet, Strategy.LargestFirstMultiAsset) - builder.add_change_if_needed(address) - } - } - } + public isValidAddress(address: string): boolean { + const { Address } = this.lib + return Address.is_valid(address) } public getAddressKeyHash(address: Address): Result { @@ -210,35 +229,43 @@ class Cardano { } public createTxBuilder(protocolParameters: ProtocolParams): TransactionBuilder { - const { BigNum, TransactionBuilder, TransactionBuilderConfigBuilder, LinearFee } = this.lib + const { BigNum, ExUnitPrices, UnitInterval, TransactionBuilder, TransactionBuilderConfigBuilder, LinearFee } = this.lib const { minFeeA, minFeeB, poolDeposit, keyDeposit, - coinsPerUtxoByte, maxTxSize, maxValSize } = protocolParameters + coinsPerUtxoByte, maxTxSize, maxValSize, maxCollateralInputs, + priceMem, priceStep, collateralPercent } = protocolParameters - if (!coinsPerUtxoByte) throw new Error('No coinsPerUtxoByte') - if (!maxValSize) throw new Error('No maxValSize') + if (!coinsPerUtxoByte) throw new Error('coinsPerUtxoByte is missing') + if (!maxValSize) throw new Error('maxValSize is missing') + if (!priceMem) throw new Error('priceMem is missing') + if (!priceStep) throw new Error('priceStep is missing') + if (!collateralPercent) throw new Error('collateralPercent is missing') + if (!maxCollateralInputs) throw new Error('maxCollateralInputs is missing') const toBigNum = (value: number) => BigNum.from_str(value.toString()) + const priceMemFraction: Fraction = new Fraction(priceMem) + const priceStepFraction: Fraction = new Fraction(priceStep) + const exUnitPrices = ExUnitPrices.new( + UnitInterval.new(toBigNum(priceMemFraction.numerator), toBigNum(priceMemFraction.denominator)), + UnitInterval.new(toBigNum(priceStepFraction.numerator), toBigNum(priceStepFraction.denominator)) + ) const config = TransactionBuilderConfigBuilder.new() .fee_algo(LinearFee.new(toBigNum(minFeeA), toBigNum(minFeeB))) .pool_deposit(toBigNum(poolDeposit)) .key_deposit(toBigNum(keyDeposit)) - .coins_per_utxo_word(toBigNum(coinsPerUtxoByte)) + .coins_per_utxo_byte(toBigNum(coinsPerUtxoByte)) .max_tx_size(maxTxSize) .max_value_size(parseFloat(maxValSize)) + .ex_unit_prices(exUnitPrices) + .collateral_percentage(collateralPercent) + .max_collateral_inputs(maxCollateralInputs) .build() return TransactionBuilder.new(config) } - public hashScript(script: NativeScript): ScriptHash { - const { ScriptHashNamespace } = this.lib - return script.hash(ScriptHashNamespace.NativeScript) - } - public getScriptAddress(script: NativeScript, isMainnet: boolean): Address { const { NetworkInfo } = this.lib - const scriptHash = this.hashScript(script) const networkInfo = isMainnet ? NetworkInfo.mainnet() : NetworkInfo.testnet() - return this.getScriptHashBaseAddress(scriptHash, networkInfo).to_address() + return this.getScriptHashBaseAddress(script.hash(), networkInfo).to_address() } public getScriptType(script: NativeScript): MultiSigType { @@ -263,36 +290,6 @@ class Cardano { } } - public buildUTxOSet(utxos: TransactionOutput[]): TransactionUnspentOutputs { - const { Address, AssetName, BigNum, MultiAsset, ScriptHash, - TransactionInput, TransactionHash, TransactionOutput, - TransactionUnspentOutput, TransactionUnspentOutputs } = this.lib - - const utxosSet = TransactionUnspentOutputs.new() - utxos.forEach((utxo) => { - const value = this.lib.Value.new(BigNum.from_str(utxo.value.toString())) - const address = Address.from_bech32(utxo.address) - if (utxo.tokens.length > 0) { - const multiAsset = MultiAsset.new() - utxo.tokens.forEach((token) => { - const { assetId } = token.asset - const policyId = ScriptHash.from_bytes(Buffer.from(getPolicyId(assetId), 'hex')) - const assetName = AssetName.new(Buffer.from(getAssetName(assetId), 'hex')) - const quantity = BigNum.from_str(token.quantity.toString()) - multiAsset.set_asset(policyId, assetName, quantity) - }) - value.set_multiasset(multiAsset) - } - const txUnspentOutput = TransactionUnspentOutput.new( - TransactionInput.new(TransactionHash.from_bytes(Buffer.from(utxo.txHash, 'hex')), BigNum.from_str(utxo.index.toString())), - TransactionOutput.new(address, value) - ) - utxosSet.add(txUnspentOutput) - }) - - return utxosSet - } - private getScriptHashBaseAddress(scriptHash: ScriptHash, networkInfo: NetworkInfo): BaseAddress { const { BaseAddress, StakeCredential } = this.lib const networkId = networkInfo.network_id() diff --git a/src/cardano/query-api.test.tsx b/src/cardano/query-api.test.tsx index 36dea0e0..cfe28727 100644 --- a/src/cardano/query-api.test.tsx +++ b/src/cardano/query-api.test.tsx @@ -73,6 +73,10 @@ describe('GraphQL API', () => { expect(params.keyDeposit).toBe(2000000) expect(params.maxTxSize).toBe(16384) expect(params.maxValSize).toBe('5000') + expect(params.priceMem).toBe(0.0577) + expect(params.priceStep).toBe(0.0000721) + expect(params.collateralPercent).toBe(150) + expect(params.maxCollateralInputs).toBe(3) } } }) diff --git a/src/cardano/query-api.ts b/src/cardano/query-api.ts index 18485be3..52f94ab3 100644 --- a/src/cardano/query-api.ts +++ b/src/cardano/query-api.ts @@ -71,6 +71,10 @@ query getUTxOsToSpend($addresses: [String]!) { coinsPerUtxoByte maxValSize maxTxSize + priceMem + priceStep + collateralPercent + maxCollateralInputs } } } diff --git a/src/components/layout.tsx b/src/components/layout.tsx index 782a0d2a..e3f37d6a 100644 --- a/src/components/layout.tsx +++ b/src/components/layout.tsx @@ -257,14 +257,14 @@ const Layout: FC<{
{!config.isMainnet &&
You are using testnet
} -
- -
{children}
+
+ +
) } diff --git a/src/components/native-script.tsx b/src/components/native-script.tsx index 0cec8400..f544f47f 100644 --- a/src/components/native-script.tsx +++ b/src/components/native-script.tsx @@ -10,6 +10,7 @@ import { useContext } from 'react' import { ConfigContext } from '../cardano/config' function minSlot(slots: Array): BigNum | undefined { + if (slots.length === 0) return return slots.reduce((prev, cur) => { if (!cur) return prev if (!prev) return cur @@ -19,6 +20,7 @@ function minSlot(slots: Array): BigNum | undefined { } function maxSlot(slots: Array): BigNum | undefined { + if (slots.length === 0) return return slots.reduce((prev, cur) => { if (!cur) return prev if (!prev) return cur diff --git a/src/components/notification.tsx b/src/components/notification.tsx index 6e5ad016..5fe42f8a 100644 --- a/src/components/notification.tsx +++ b/src/components/notification.tsx @@ -84,7 +84,7 @@ const Notification: FC<{ }) const getClassName = (): string => { - const base = 'rounded-md shadow overflow-hidden relative' + const base = 'rounded shadow overflow-hidden relative' switch (type) { case 'success': return `${base} bg-green-100 text-green-500` diff --git a/src/components/transaction.tsx b/src/components/transaction.tsx index 16845748..483a0e2a 100644 --- a/src/components/transaction.tsx +++ b/src/components/transaction.tsx @@ -4,7 +4,6 @@ import { AssetAmount, ADAAmount } from './currency' import { decodeASCII, getAssetName } from '../cardano/query-api' import { Cardano, getResult, toHex, toIter, useCardanoMultiplatformLib, verifySignature } from '../cardano/multiplatform-lib' import type { Recipient } from '../cardano/multiplatform-lib' -import type { Result } from '../cardano/multiplatform-lib' import type { Address, NativeScript, Transaction, TransactionBody, TransactionHash, Vkeywitness } from '@dcspark/cardano-multiplatform-lib-browser' import { DocumentDuplicateIcon, MagnifyingGlassCircleIcon, ShareIcon, ArrowUpTrayIcon } from '@heroicons/react/24/solid' import Link from 'next/link' @@ -25,14 +24,10 @@ import { estimateSlotByDate } from '../cardano/utils' const TransactionReviewButton: FC<{ className?: string - result: Result -}> = ({ className, result }) => { - if (!result.isOk) return ( -
Review Transaction
- ) - + transaction: Transaction +}> = ({ className, transaction }) => { return ( - + Review Transaction ) @@ -156,11 +151,10 @@ const AddressViewer: FC<{ } const NativeScriptInfoViewer: FC<{ - cardano: Cardano className?: string script: NativeScript -}> = ({ cardano, className, script }) => { - const hash = cardano.hashScript(script) +}> = ({ className, script }) => { + const hash = script.hash() const treasury = useLiveQuery(async () => db.treasuries.get(hash.to_hex()), [script]) if (!treasury) return ( @@ -183,12 +177,11 @@ const NativeScriptInfoViewer: FC<{ } const DeleteTreasuryButton: FC<{ - cardano: Cardano className?: string children: ReactNode script: NativeScript -}> = ({ cardano, className, children, script }) => { - const hash = cardano.hashScript(script) +}> = ({ className, children, script }) => { + const hash = script.hash() const treasury = useLiveQuery(async () => db.treasuries.get(hash.to_hex()), [script]) const router = useRouter() @@ -418,19 +411,18 @@ const WalletInfo: FC<{ } const SaveTreasuryButton: FC<{ - cardano: Cardano className?: string children: ReactNode name: string description: string script?: NativeScript -}> = ({ cardano, name, description, script, className, children }) => { +}> = ({ name, description, script, className, children }) => { const router = useRouter() const { notify } = useContext(NotificationContext) if (!script) return ; - const hash = cardano.hashScript(script).to_hex() + const hash = script.hash().to_hex() const submitHandle = () => { db diff --git a/src/components/user-data.tsx b/src/components/user-data.tsx index 4c2284cb..be41020d 100644 --- a/src/components/user-data.tsx +++ b/src/components/user-data.tsx @@ -123,7 +123,7 @@ const ImportUserData: FC = () => { const treasuries: Treasury[] = userData.treasuries.map((treasury) => { const script = Buffer.from(treasury.script, 'base64') const nativeScript = cardano.lib.NativeScript.from_bytes(script) - const hash = cardano.hashScript(nativeScript).to_hex() + const hash = nativeScript.hash().to_hex() return { hash, name: treasury.name, diff --git a/src/pages/open.tsx b/src/pages/open.tsx index edadda05..600967c9 100644 --- a/src/pages/open.tsx +++ b/src/pages/open.tsx @@ -45,9 +45,9 @@ const GetTransaction: NextPage = () => { onChange={(e) => setText(e.target.value)} placeholder='URL or CBOR in Hex'> -
- {txResult && } -
+ {txResult?.isOk &&
+ +
} ) diff --git a/src/pages/treasuries/[base64CBOR]/edit.tsx b/src/pages/treasuries/[base64CBOR]/edit.tsx index 74585d57..ac7b1de0 100644 --- a/src/pages/treasuries/[base64CBOR]/edit.tsx +++ b/src/pages/treasuries/[base64CBOR]/edit.tsx @@ -19,7 +19,7 @@ const EditTreasury: FC<{ router: NextRouter script: NativeScript }> = ({ cardano, script }) => { - const hash = cardano.hashScript(script) + const hash = script.hash() const treasury = useLiveQuery(async () => db.treasuries.get(hash.to_hex()), []) const [name, setName] = useState('') const [description, setDescription] = useState('') @@ -72,7 +72,6 @@ const EditTreasury: FC<{
Delete @@ -80,7 +79,6 @@ const EditTreasury: FC<{
Back
- +
Treasury Address
diff --git a/src/pages/treasuries/[base64CBOR]/new.tsx b/src/pages/treasuries/[base64CBOR]/new.tsx index 631b8ce3..7a2ef3d1 100644 --- a/src/pages/treasuries/[base64CBOR]/new.tsx +++ b/src/pages/treasuries/[base64CBOR]/new.tsx @@ -1,21 +1,21 @@ import type { NextPage } from 'next' -import type { FC } from 'react' import { useRouter } from 'next/router' -import { BackButton, Layout, Panel } from '../../../components/layout' -import { Cardano, isAddressNetworkCorrect, newRecipient, Recipient } from '../../../cardano/multiplatform-lib' -import { getResult, useCardanoMultiplatformLib } from '../../../cardano/multiplatform-lib' +import { BackButton, Hero, Layout, Panel } from '../../../components/layout' +import { Cardano, isAddressNetworkCorrect, newRecipient, Recipient, getResult, useCardanoMultiplatformLib } from '../../../cardano/multiplatform-lib' import { ErrorMessage, Loading } from '../../../components/status' -import type { ChangeEventHandler } from 'react' -import { useContext, useMemo, useState } from 'react' +import type { FC, ChangeEventHandler } from 'react' +import { useContext, useMemo, useState, useCallback, useEffect } from 'react' import { ConfigContext } from '../../../cardano/config' import { NativeScriptInfoViewer, TransactionReviewButton } from '../../../components/transaction' -import { decodeASCII, getAssetName, getBalanceByUTxOs, useGetUTxOsToSpendQuery } from '../../../cardano/query-api' -import type { NativeScript } from '@dcspark/cardano-multiplatform-lib-browser' import type { Value } from '../../../cardano/query-api' +import { decodeASCII, getAssetName, getBalanceByUTxOs, getPolicyId, useGetUTxOsToSpendQuery } from '../../../cardano/query-api' +import type { NativeScript } from '@dcspark/cardano-multiplatform-lib-browser' import type { ProtocolParams, TransactionOutput } from '@cardano-graphql/client-ts' -import { PlusIcon, TrashIcon, XMarkIcon } from '@heroicons/react/24/solid' +import { PlusIcon, TrashIcon, XCircleIcon, XMarkIcon } from '@heroicons/react/24/solid' import { ADAAmount, getADASymbol, LabeledCurrencyInput } from '../../../components/currency' import { suggestExpirySlot, suggestStartSlot } from '../../../components/native-script' +import type { Output } from 'cardano-utxo-wasm' +import init, { select } from 'cardano-utxo-wasm' const AddAssetButton: FC<{ budget: Value @@ -56,6 +56,33 @@ const AddAssetButton: FC<{ ) } +const RecipientAddressInput: FC<{ + address: string + cardano: Cardano + className?: string + disabled?: boolean + setAddress: (address: string) => void +}> = ({ address, cardano, className, disabled, setAddress }) => { + const [config, _] = useContext(ConfigContext) + + const isValid = cardano.isValidAddress(address) && isAddressNetworkCorrect(config, cardano.parseAddress(address)) + + return ( +
+ + {address && !isValid &&

The address is invalid.

} +
+ ) +} + const TransactionRecipient: FC<{ cardano: Cardano recipient: Recipient @@ -93,26 +120,11 @@ const TransactionRecipient: FC<{ }) } - const minLovelace = getMinLovelace(recipient) - const addressResult = getResult(() => { - const addressObject = cardano.lib.Address.from_bech32(address) - if (!isAddressNetworkCorrect(config, addressObject)) throw new Error('This address is from a wrong network') - return addressObject - }) + const minLovelace = cardano.isValidAddress(address) ? getMinLovelace(recipient) : undefined return (
-
- - {address && !addressResult.isOk &&

{addressResult.message}

} -
+
-

+ {minLovelace ?

At least is required -

+

: null}
    {Array.from(value.assets).map(([id, quantity]) => { @@ -185,48 +197,118 @@ const NewTransaction: FC<{ protocolParameters: ProtocolParams nativeScript: NativeScript utxos: TransactionOutput[] -}> = ({ cardano, protocolParameters, nativeScript, utxos }) => { + minLovelace: bigint +}> = ({ cardano, protocolParameters, nativeScript, utxos, minLovelace }) => { const [recipients, setRecipients] = useState([newRecipient()]) const [message, setMessage] = useState([]) const [config, _] = useContext(ConfigContext) + const [inputs, setInputs] = useState([]) + const defaultChangeAddress = useMemo(() => cardano.getScriptAddress(nativeScript, config.isMainnet).to_bech32(), [cardano, nativeScript, config]) + const [changeAddress, setChangeAddress] = useState(defaultChangeAddress) + const [isChangeSettingDisabled, setIsChangeSettingDisabled] = useState(true) + const [willSpendAll, setWillSpendAll] = useState(false) - const getMinLovelace = (recipient: Recipient): bigint => { - const coinsPerUtxoByte = protocolParameters.coinsPerUtxoByte - if (!coinsPerUtxoByte) throw new Error('No coinsPerUtxoByte') - return cardano.getMinLovelace(recipient.value, false, coinsPerUtxoByte) - } + useEffect(() => { + let isMounted = true + + if (isMounted && isChangeSettingDisabled) { + setChangeAddress(defaultChangeAddress) + setWillSpendAll(false) + } + + return () => { + isMounted = false + } + }, [defaultChangeAddress, isChangeSettingDisabled]) + + useEffect(() => { + let isMounted = true + + if (isMounted) { + if (willSpendAll || recipients.length === 0) { + setInputs(utxos) + return + } + + setInputs([]) - const budget: Value = recipients - .map(({ value }) => value) - .reduce((result, value) => { - const lovelace = result.lovelace - value.lovelace - const assets = new Map(result.assets) - Array.from(value.assets).forEach(([id, quantity]) => { - const _quantity = assets.get(id) - _quantity && assets.set(id, _quantity - quantity) + init().then(() => { + const inputs: Output[] = utxos.map((txOutput) => { + return { + data: txOutput, + lovelace: BigInt(txOutput.value), + assets: txOutput.tokens.map((token) => { + const assetId = token.asset.assetId + return { + policyId: getPolicyId(assetId), + assetName: getAssetName(assetId), + quantity: BigInt(token.quantity) + } + }) + } + }) + const outputs: Output[] = recipients.map((recipient) => { + return { + lovelace: recipient.value.lovelace, + assets: Array.from(recipient.value.assets).map(([id, quantity]) => { + return { + policyId: getPolicyId(id), + assetName: getAssetName(id), + quantity: BigInt(quantity) + } + }) + } + }) + const result = select(inputs, outputs, { lovelace: minLovelace, assets: [] }) + const txOutputs: TransactionOutput[] | undefined = result?.selected.map((output) => output.data) + txOutputs && setInputs(txOutputs) }) - return { lovelace, assets } - }, getBalanceByUTxOs(utxos)) + } - const transactionResult = useMemo(() => { - const { NativeScripts } = cardano.lib - const txBuilder = cardano.createTxBuilder(protocolParameters) - const nativeScripts = NativeScripts.new() - nativeScripts.add(nativeScript) - txBuilder.set_native_scripts(nativeScripts) + return () => { + isMounted = false + } + }, [utxos, recipients, willSpendAll, minLovelace]) + + const getMinLovelace = useCallback((recipient: Recipient): bigint => cardano.getMinLovelace(recipient, protocolParameters), [cardano, protocolParameters]) + + const budget: Value = useMemo(() => recipients + .map(({ value }) => value) + .reduce((result, value) => { + const lovelace = result.lovelace - value.lovelace + const assets = new Map(result.assets) + Array.from(value.assets).forEach(([id, quantity]) => { + const _quantity = assets.get(id) + _quantity && assets.set(id, _quantity - quantity) + }) + return { lovelace, assets } + }, getBalanceByUTxOs(utxos)), [recipients, utxos]) + + const txResult = useMemo(() => getResult(() => { + if (inputs.length === 0) throw new Error('No UTxO is spent.') + + const { AuxiliaryData, ChangeSelectionAlgo, NativeScriptWitnessInfo, MetadataJsonSchema } = cardano.lib + const txBuilder = cardano.createTxBuilder(protocolParameters) + + inputs.forEach((input) => { + const result = cardano + .createTxInputBuilder(input) + .native_script(nativeScript, NativeScriptWitnessInfo.assume_signature_count()) + txBuilder.add_input(result) + }) - return getResult(() => { recipients.forEach((recipient) => { - const txOutputResult = cardano.buildTxOutput(recipient) - if (!txOutputResult?.isOk) throw new Error('There are some invalid Transaction Outputs') - txBuilder.add_output(txOutputResult.data) + const result = cardano.buildTxOutput(recipient, protocolParameters) + txBuilder.add_output(result) }) if (message.length > 0) { const value = JSON.stringify({ msg: message }) - txBuilder.add_json_metadatum(cardano.getMessageLabel(), value) + let auxiliaryData = AuxiliaryData.new() + auxiliaryData.add_json_metadatum_with_schema(cardano.getMessageLabel(), value, MetadataJsonSchema.NoConversions) + txBuilder.add_auxiliary_data(auxiliaryData) } const startSlot = suggestStartSlot(nativeScript) @@ -239,83 +321,110 @@ const NewTransaction: FC<{ txBuilder.set_ttl(expirySlot) } - const address = cardano.getScriptAddress(nativeScript, config.isMainnet) - cardano.chainCoinSelection(txBuilder, cardano.buildUTxOSet(utxos), address) - - return txBuilder.build_tx() - }) - }, [recipients, cardano, config, message, protocolParameters, utxos, nativeScript]) + return txBuilder.build(ChangeSelectionAlgo.Default, cardano.parseAddress(changeAddress)).build_unchecked() + }), [recipients, cardano, changeAddress, message, protocolParameters, inputs, nativeScript]) - const handleRecipientChange = (recipient: Recipient) => { - setRecipients(recipients.map((_recipient) => _recipient.id === recipient.id ? recipient : _recipient)) - } + const handleRecipientChange = (recipient: Recipient) => { + setRecipients(recipients.map((_recipient) => _recipient.id === recipient.id ? recipient : _recipient)) + } - const deleteRecipient = (recipient: Recipient) => { - setRecipients(recipients.filter(({ id }) => id !== recipient.id)) - } + const deleteRecipient = (recipient: Recipient) => { + setRecipients(recipients.filter(({ id }) => id !== recipient.id)) + } - return ( - -
      - {recipients.map((recipient, index) => -
    • -
      -

      Recipient #{index + 1}

      -
      + +
    • + )} +
    +
    +
    +

    {recipients.length > 0 ? 'Change' : 'Send All'}

    +

    {recipients.length > 0 ? 'The change caused by this transaction or all remaining assets in the treasury will be sent to this address (default to the treasury address). DO NOT MODIFY IT UNLESS YOU KNOW WHAT YOU ARE DOING!' : 'All assets in this treasury will be sent to this address.'}

    + {recipients.length > 0 &&

    + +

    } +
    +
    + - - )} -
-
-
-

Message

-

Cannot exceed 64 bytes each line

-
- -
-
-
- {transactionResult.isOk && -

- Fee: - -

- } + disabled={isChangeSettingDisabled && recipients.length > 0} + address={changeAddress} + setAddress={setChangeAddress} /> + {!isChangeSettingDisabled && recipients.length > 0 &&
+ +
} +
- -
- - ) -} +
+
+

Message

+

Cannot exceed 64 bytes each line.

+
+ +
+
+
+ {txResult.isOk &&

+ Fee: + +

} + {!txResult.isOk &&

+ + {txResult.message === 'The address is invalid.' ? 'Some addresses are invalid.' : txResult.message} +

} +
+ +
+ + ) + } const GetUTxOsToSpend: FC<{ cardano: Cardano script: NativeScript }> = ({ cardano, script }) => { - + const minLovelace = BigInt(5e6) const [config, _] = useContext(ConfigContext) const address = cardano.getScriptAddress(script, config.isMainnet) const { loading, error, data } = useGetUTxOsToSpendQuery({ @@ -333,12 +442,19 @@ const GetUTxOsToSpend: FC<{ return (
+ +

Create Transaction

+
+

Due to the native assets, you should have at least in your treasury in order to create transactions properly.

+

You can send all assets to other by removing all the recipients.

+
+
diff --git a/src/pages/treasuries/new.tsx b/src/pages/treasuries/new.tsx index 54aca369..e3dd0db3 100644 --- a/src/pages/treasuries/new.tsx +++ b/src/pages/treasuries/new.tsx @@ -419,7 +419,6 @@ const NewTreasury: NextPage = () => {