Skip to content

Commit

Permalink
Upgrade CML (#105)
Browse files Browse the repository at this point in the history
* Update package.json

* Upgrade CML
* Install cardano-utxo-wasm
* Patch updates

* Update txBuilder

Use cardano-utxo-wasm for coin selection.

* Fix error of empty array reduction

* Fix import type

* Apply error message of tx result

* Improve parseAddress

* Fix open page

* Add tests

* Throw error for insufficient ADA

* Add min ADA notice

* Fix typo

* Extract RecipientAddressInput

* Add change address input

* Simplify RecipientAddressInput

* Improve error message

* Protect change address setting

* Add period

* Use label tag

* Improve createTxInputBuilder

* Fix deps

* Add send all option

* Fix output builder

* Fix deps

* Allow zero recipient

* Remove duplicated imports

* Return to the default

* Wrap label tag in p tag

* Improve the tips

* Improve the warning message

* Rename variables

* Implement auto send all

* Fix single ADA calculation

* Add note

* Fix minimum ADA calculation

* Fix the text space

* Fix notification

* Fix send all
  • Loading branch information
siegfried authored Oct 31, 2022
1 parent a32720d commit c57bc1c
Show file tree
Hide file tree
Showing 17 changed files with 379 additions and 251 deletions.
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
159 changes: 78 additions & 81 deletions src/cardano/multiplatform-lib.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -62,7 +65,7 @@ function toIter<T>(set: CardanoIterable<T>): IterableIterator<T> {
value: set.get(index++)
} : { done: true, value: null }
},
[Symbol.iterator]: function() { return this }
[Symbol.iterator]: function () { return this }
}
}

Expand Down Expand Up @@ -101,29 +104,35 @@ class Cardano {
return this._wasm
}

public buildTxOutput(recipient: Recipient): Result<CardanoTransactionOutput> {
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()))
Expand All @@ -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')
}
Expand Down Expand Up @@ -168,29 +198,18 @@ class Cardano {
return toHex(witnessSet)
}

public parseAddress(bech32Address: string): Result<Address> {
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<Ed25519KeyHash> {
Expand All @@ -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 {
Expand All @@ -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()
Expand Down
4 changes: 4 additions & 0 deletions src/cardano/query-api.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
})
Expand Down
4 changes: 4 additions & 0 deletions src/cardano/query-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ query getUTxOsToSpend($addresses: [String]!) {
coinsPerUtxoByte
maxValSize
maxTxSize
priceMem
priceStep
collateralPercent
maxCollateralInputs
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/components/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -257,14 +257,14 @@ const Layout: FC<{
<SecondaryBar />
<div className='w-full bg-sky-100 overflow-y-auto'>
{!config.isMainnet && <div className='p-1 bg-red-900 text-white text-center'>You are using testnet</div>}
<div className='flex flex-row-reverse'>
<NotificationCenter className='fixed space-y-2 w-1/4' />
</div>
<div className='p-2 h-screen space-y-2'>
<ChainProgress />
{children}
</div>
</div>
<div className='flex flex-row-reverse'>
<NotificationCenter className='fixed space-y-2 w-1/4 p-4' />
</div>
</div>
)
}
Expand Down
2 changes: 2 additions & 0 deletions src/components/native-script.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useContext } from 'react'
import { ConfigContext } from '../cardano/config'

function minSlot(slots: Array<BigNum | undefined>): BigNum | undefined {
if (slots.length === 0) return
return slots.reduce((prev, cur) => {
if (!cur) return prev
if (!prev) return cur
Expand All @@ -19,6 +20,7 @@ function minSlot(slots: Array<BigNum | undefined>): BigNum | undefined {
}

function maxSlot(slots: Array<BigNum | undefined>): BigNum | undefined {
if (slots.length === 0) return
return slots.reduce((prev, cur) => {
if (!cur) return prev
if (!prev) return cur
Expand Down
2 changes: 1 addition & 1 deletion src/components/notification.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
26 changes: 9 additions & 17 deletions src/components/transaction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -25,14 +24,10 @@ import { estimateSlotByDate } from '../cardano/utils'

const TransactionReviewButton: FC<{
className?: string
result: Result<Transaction>
}> = ({ className, result }) => {
if (!result.isOk) return (
<div className={['text-gray-400 bg-gray-100 border cursor-not-allowed', className].join(' ')}>Review Transaction</div>
)

transaction: Transaction
}> = ({ className, transaction }) => {
return (
<Link href={getTransactionPath(result.data)}>
<Link href={getTransactionPath(transaction)}>
<a className={['text-white bg-sky-700', className].join(' ')}>Review Transaction</a>
</Link>
)
Expand Down Expand Up @@ -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 (
Expand All @@ -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()

Expand Down Expand Up @@ -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 <button className={className} disabled={true}>{children}</button>;

const hash = cardano.hashScript(script).to_hex()
const hash = script.hash().to_hex()

const submitHandle = () => {
db
Expand Down
Loading

0 comments on commit c57bc1c

Please sign in to comment.