diff --git a/public/background.js b/public/background.js index 8c10c678..f96809d6 100644 --- a/public/background.js +++ b/public/background.js @@ -4,114 +4,142 @@ var popupWindowId = false var connectWindowId = false -chrome.runtime.onMessageExternal.addListener(function ( - request, - sender, - sendResponse, -) { - if (request) { - if (request.message) { - if (request.message === 'version') { - sendResponse({ version: chrome.runtime.getManifest().version }) - } +chrome.runtime.onMessageExternal.addListener( + function (request, sender, sendResponse) { + if (request) { + if (request.message) { + if (request.message === 'version') { + sendResponse({ version: chrome.runtime.getManifest().version }) + } - if (request.message === 'connect') { - if (connectWindowId === false) { - popupWindowId = true - chrome.windows.create( - { - url: chrome.runtime.getURL('popup.html'), - type: 'popup', - width: 800, - height: 600, - focused: true, - }, - function (win) { - connectWindowId = win.id - setTimeout(function () { - chrome.runtime.sendMessage( - { - action: 'connect', - }, - function (response) { - sendResponse(response) - }, - ) - }, 1000) - }, - ) - } else if (typeof popupWindowId === 'number') { - //The window is open, and the user clicked the button. - // Focus the window. - chrome.windows.update(popupWindowId, { focused: true }) + if (request.message === 'connect') { + if (connectWindowId === false) { + popupWindowId = true + chrome.windows.create( + { + url: chrome.runtime.getURL('popup.html'), + type: 'popup', + width: 800, + height: 600, + focused: true, + }, + function (win) { + connectWindowId = win.id + setTimeout(function () { + chrome.runtime.sendMessage( + { + action: 'connect', + }, + function (response) { + sendResponse(response) + }, + ) + }, 1000) + }, + ) + } else if (typeof popupWindowId === 'number') { + //The window is open, and the user clicked the button. + // Focus the window. + chrome.windows.update(popupWindowId, { focused: true }) + } } - } - if (request.message === 'delegate') { - if (popupWindowId === false) { - popupWindowId = true - chrome.windows.create( - { - url: chrome.runtime.getURL('popup.html'), - type: 'popup', - width: 800, - height: 600, - focused: true, - }, - function (win) { - popupWindowId = win.id - setTimeout(function () { - chrome.runtime.sendMessage({ - action: 'createDelegate', - data: { - pool_id: request.pool_id, - referral_code: request.referral_code || '', - }, - }) - }, 1000) - }, - ) - } else if (typeof popupWindowId === 'number') { - //The window is open, and the user clicked the button. - // Focus the window. - chrome.windows.update(popupWindowId, { focused: true }) + if (request.message === 'delegate') { + if (popupWindowId === false) { + popupWindowId = true + chrome.windows.create( + { + url: chrome.runtime.getURL('popup.html'), + type: 'popup', + width: 800, + height: 600, + focused: true, + }, + function (win) { + popupWindowId = win.id + setTimeout(function () { + chrome.runtime.sendMessage({ + action: 'createDelegate', + data: { + pool_id: request.pool_id, + referral_code: request.referral_code || '', + }, + }) + }, 1000) + }, + ) + } else if (typeof popupWindowId === 'number') { + //The window is open, and the user clicked the button. + // Focus the window. + chrome.windows.update(popupWindowId, { focused: true }) + } } - } - if (request.message === 'stake') { - if (popupWindowId === false) { - popupWindowId = true - chrome.windows.create( - { - url: chrome.runtime.getURL('popup.html'), - type: 'popup', - width: 800, - height: 630, - focused: true, - }, - function (win) { - popupWindowId = win.id - setTimeout(function () { - chrome.runtime.sendMessage({ - action: 'addStake', - data: { - delegation_id: request.delegation_id, - amount: request.amount, - }, - }) - }, 1000) - }, - ) - } else if (typeof popupWindowId === 'number') { - //The window is open, and the user clicked the button. - // Focus the window. - chrome.windows.update(popupWindowId, { focused: true }) + if (request.message === 'output') { + if (popupWindowId === false) { + popupWindowId = true + chrome.windows.create( + { + url: chrome.runtime.getURL('popup.html'), + type: 'popup', + width: 800, + height: 600, + focused: true, + }, + function (win) { + popupWindowId = win.id + setTimeout(function () { + chrome.runtime.sendMessage({ + action: 'customOutput', + data: { + output: request.output, + }, + }) + }, 1000) + }, + ) + } else if (typeof popupWindowId === 'number') { + //The window is open, and the user clicked the button. + // Focus the window. + chrome.windows.update(popupWindowId, { focused: true }) + } + } + + if (request.message === 'stake') { + if (popupWindowId === false) { + popupWindowId = true + chrome.windows.create( + { + url: chrome.runtime.getURL('popup.html'), + type: 'popup', + width: 800, + height: 630, + focused: true, + }, + function (win) { + popupWindowId = win.id + setTimeout(function () { + chrome.runtime.sendMessage({ + action: 'addStake', + data: { + delegation_id: request.delegation_id, + amount: request.amount, + }, + }) + }, 1000) + }, + ) + } else if (typeof popupWindowId === 'number') { + //The window is open, and the user clicked the button. + // Focus the window. + chrome.windows.update(popupWindowId, { focused: true }) + } } } } - } - return true -}) + return true + }, +) chrome.windows.onRemoved.addListener(function (winId) { if (popupWindowId === winId) { diff --git a/src/components/composed/Navigation/Navigation.js b/src/components/composed/Navigation/Navigation.js index f9f0f0e1..56b9d454 100644 --- a/src/components/composed/Navigation/Navigation.js +++ b/src/components/composed/Navigation/Navigation.js @@ -40,6 +40,11 @@ const Navigation = ({ customNavigation }) => { toggleSliderMenu() } + const goCustomOutput = () => { + navigate('/wallet/Mintlayer/custom-output') + toggleSliderMenu() + } + const goDashboard = () => { navigate('/dashboard') toggleSliderMenu() @@ -84,6 +89,12 @@ const Navigation = ({ customNavigation }) => { icon: , onClick: goSettings, }, + { + id: 2, + label: 'Custom Output', + icon: , + onClick: goCustomOutput, + }, ] const navigationList = [ diff --git a/src/index.js b/src/index.js index ce830a06..1bf5e5d2 100644 --- a/src/index.js +++ b/src/index.js @@ -27,6 +27,7 @@ import { DelegationStakePage, DelegationWithdrawPage, LockedBalancePage, + SendCustomOutput, } from '@Pages' import { @@ -129,6 +130,27 @@ const App = () => { }) } + if (request.action === 'customOutput') { + if (!unlocked) { + setNextAfterUnlock({ + route: '/wallet/Mintlayer/custom-output', + state: { + action: 'customOutput', + output: request.data.output, + }, + }) + return + } + // change route to staking page + navigate('/wallet/Mintlayer/staking/create-delegation', { + state: { + action: 'createDelegate', + pool_id: request.data.pool_id, + referral_code: request.data.referral_code || '', + }, + }) + } + if (request.action === 'getAddresses') { // respond with addresses sendResponse({ @@ -219,6 +241,10 @@ const App = () => { path="/wallet/:coinType/send-transaction" element={} /> + } + /> } diff --git a/src/pages/SendCustomOutput/SendCustomOutput.helpers.js b/src/pages/SendCustomOutput/SendCustomOutput.helpers.js new file mode 100644 index 00000000..be2ff592 --- /dev/null +++ b/src/pages/SendCustomOutput/SendCustomOutput.helpers.js @@ -0,0 +1,129 @@ +export const validateOutput = (output) => { + // validate output + return true +} + +export const getInfoAboutOutput = (output) => { + const parsedOutput = JSON.parse(output) + + if (parsedOutput.type === 'Transfer') { + return { + type: 'Transfer', + requiredFields: ['destination', 'value'], + amountToSend: parsedOutput.value.amount.decimal, + allFields: { + destination: parsedOutput.destination, + 'value.amount.decimal': parsedOutput.value.amount.decimal, + } + } + } + + if(parsedOutput.type === 'IssueFungibleToken') { + return { + type: 'IssueFungibleToken', + requiredFields: ['authority'], + extraFee: 100 * 1e11, // Decimal + allFields: { + authority: parsedOutput.authority, + is_freezable: parsedOutput.is_freezable, + 'metadata_uri.string': parsedOutput.metadata_uri.string, + number_of_decimals: parsedOutput.number_of_decimals, + 'token_ticker.string': parsedOutput.token_ticker.string, + 'total_supply.amount.decimal': parsedOutput.total_supply.amount.decimal, + 'total_supply.type': parsedOutput.total_supply.type, + } + } + } + + if(parsedOutput.type === 'IssueNft') { + return { + type: 'IssueNft', + requiredFields: ['authority'], + extraFee: 50 * 1e11, // Decimal + allFields: { + token_id: parsedOutput.token_id, + destination: parsedOutput.destination, + 'data.name': parsedOutput.data.name, + 'data.ticker': parsedOutput.data.ticker, + } + } + } + + if(parsedOutput.type === 'DataDeposit') { + return { + type: 'DataDeposit', + requiredFields: ['authority'], + extraFee: 50 * 1e11, // Decimal + allFields: { + data: parsedOutput.data, + } + } + } + + return {} +} + +export const getTemplate = (name) => { + if(name === 'Transfer') { + return { + type: 'Transfer', + value: { + type: 'Coin', + amount: { + atoms: '', + decimal: '', + } + }, + destination: 'insert destination address', + } + } + if(name === 'IssueFungibleToken') { + return { + type: 'IssueFungibleToken', + //authority: '_AUTHORITY', + authority: 'tmt1q9r4gz3aevjm38yq8ycd6gl3kqd25xh4jqzjthdc', + is_freezable: false, + metadata_uri: { + hex: '', + string: 'ipfs://bafybeid7qggkecapxm5ysv7beococ5x7zdz3ro43jfj7oxhi327k62xhsq/TKNTNXM1.json' + }, + number_of_decimals: 8, + token_ticker: { + hex: '', + string: 'KTNAA' + }, + total_supply: { + amount: { + atoms: '', + decimal: '100' + }, + type: 'Fixed' + } + } + } + if(name === 'IssueNft') { + return { + type: 'IssueNft', + token_id: '', + destination: '', + data: { + name: '', + ticker: '', + }, + } + } + if(name === 'DataDeposit') { + return { + type: 'DataDeposit', + data: '' + } + } +} + +export const stringToHex = (str) => { + let hex = '' + for (let i = 0; i < str.length; i++) { + hex += str.charCodeAt(i).toString(16) + } + return hex +} diff --git a/src/pages/SendCustomOutput/SendCustomOutput.js b/src/pages/SendCustomOutput/SendCustomOutput.js new file mode 100644 index 00000000..75b12f89 --- /dev/null +++ b/src/pages/SendCustomOutput/SendCustomOutput.js @@ -0,0 +1,338 @@ +import React, { useContext, useEffect, useState } from 'react' +import styles from './SendCustomOutput.module.css' +import { validateOutput, getInfoAboutOutput, getTemplate, stringToHex } from './SendCustomOutput.helpers' +import { AccountContext, SettingsContext } from '@Contexts' +import { AppInfo } from '@Constants' +import { useMlWalletInfo } from '@Hooks' +import { Account } from '@Entities' +import { ML } from '@Cryptos' +import { MLTransaction } from '@Helpers' +import { Mintlayer } from '@APIs' +import { LocalStorageService } from '@Storage' +import { getTransactionUtxos, totalUtxosAmount } from '../../utils/Helpers/ML/MLTransaction' + +const SendCustomOutput = () => { + const { addresses, accountID } = useContext(AccountContext) + const { networkType } = useContext(SettingsContext) + const currentMlAddresses = + networkType === AppInfo.NETWORK_TYPES.MAINNET + ? addresses.mlMainnetAddresses + : addresses.mlTestnetAddresses + + const { + balance: mlBalance, + utxos, + unusedAddresses, + feerate, + currentHeight, + } = useMlWalletInfo(currentMlAddresses) + + useEffect(() => { + // add style to body + document.body.style.height = 'auto' + document.documentElement.style.overflow = 'auto' + return () => { + document.body.style.height = '600px' + document.documentElement.style.overflow = 'hidden' + } + }, []) + + const [customOutput, setCustomOutput] = useState('') + const [fee, setFee] = useState('') + const [error, setError] = useState('') + const [inputs, setInputs] = useState([]) + const [outputs, setOutputs] = useState([]) + const [fields, setFields] = useState([]) + const [transactionHex, setTransactionHex] = useState('') + + const tplRef = React.createRef() + const passwordRef = React.createRef() + + const handleOutputChange = (e) => { + const text = e.target.value + setCustomOutput(text) + } + + const handleValidate = async () => { + // validate output + if (!validateOutput(customOutput)) { + setError('Invalid output') + return + } + + // parse output + const parsedOutput = getInfoAboutOutput(customOutput) + let amountToSend = 0 + + if(!parsedOutput) { + setError('Invalid output') + return + } + + if(parsedOutput.amountToSend) { + amountToSend = parsedOutput.amountToSend * 1e11 + } + + if(parsedOutput.requiredFields) { + // TODO go through the required fields and check and add info + } + + const adjustedOutput = JSON.parse(customOutput) + + const unusedChangeAddress = unusedAddresses.change + + const utxoCoin = utxos.filter((utxo) => utxo.utxo.value.type === 'Coin') + const inputs = getTransactionUtxos({ + utxos: utxoCoin, + amount: + (parsedOutput.extraFee || 0) + amountToSend + fee, + }) + console.log('inputs', inputs) + + setInputs(inputs) + const utxoBalance = totalUtxosAmount(utxoCoin) + console.log('utxoBalance', utxoBalance) + + const transactionFee = fee + const extraFee = parsedOutput.extraFee || 0 + + const amountToReturn = BigInt(utxoBalance) - BigInt(amountToSend) - BigInt(transactionFee) - BigInt(extraFee) + + console.log('amountToReturn', amountToReturn) + + // add change + const output = { + type: 'Transfer', + destination: unusedChangeAddress, + value: { + type: 'Coin', + amount: { + atoms: amountToReturn.toString(), + decimal: amountToReturn.toString() / 1e11, + }, + } + } + + setOutputs([adjustedOutput, output]) + + // calculate fee + const transactionSize = await MLTransaction.calculateCustomTransactionSizeInBytes({ + network: networkType, + inputs, + outputs, + currentHeight + }) + + const new_fee = Math.ceil(feerate * (transactionSize / 1000)) + setFee(new_fee) + } + + const handleBuildTransaction = async () => { + const password = passwordRef.current.value + const changeAddressesLength = currentMlAddresses.mlChangeAddresses.length + + const { mlPrivKeys } = await Account.unlockAccount(accountID, password) + const privKey = + networkType === 'mainnet' + ? mlPrivKeys.mlMainnetPrivateKey + : mlPrivKeys.mlTestnetPrivateKey + + const walletPrivKeys = await ML.getWalletPrivKeysList( + privKey, + networkType, + changeAddressesLength, + ) + const keysList = { + ...walletPrivKeys.mlReceivingPrivKeys, + ...walletPrivKeys.mlChangePrivKeys, + } + + const transactionHex = await MLTransaction.sendCustomTransaction({ + keysList: keysList, + network: networkType, + inputs: inputs, + outputs: outputs, + currentHeight, + }) + + setTransactionHex(transactionHex) + + return false + } + + const handleBroadcast = async () => { + // TODO broadcast transaction + const result = await Mintlayer.broadcastTransaction(transactionHex) + + const account = LocalStorageService.getItem('unlockedAccount') + const accountName = account.name + const unconfirmedTransactionString = `${AppInfo.UNCONFIRMED_TRANSACTION_NAME}_${accountName}_${networkType}` + const unconfirmedTransactions = + LocalStorageService.getItem(unconfirmedTransactionString) || [] + + unconfirmedTransactions.push({ + direction: 'out', + type: 'Unconfirmed', + destAddress: 'destAddress', + value: 'value', + confirmations: 0, + date: '', + txid: JSON.parse(result).tx_id, + fee: fee.toString(), + isConfirmed: false, + mode: 'mode', + usedUtxosOutpoints: inputs.map( + ({ outpoint: { index, source_id } }) => ({ index, source_id }), + ), + }) + LocalStorageService.setItem( + unconfirmedTransactionString, + unconfirmedTransactions, + ) + + return true + } + + const handleInsertTemplate = () => { + if(customOutput?.length > 0 && !window.confirm('Are you sure you want to insert a template? It will overwrite the current output')) { + return + } + const templateName = tplRef.current.selectedOptions[0].value + const template = JSON.stringify(getTemplate(templateName), null, 2) + setCustomOutput(template) + } + + const handleTogglePreview = () => { + // TODO toggle preview + } + + useEffect(() => { + if(customOutput) { + const allFields = getInfoAboutOutput(customOutput).allFields + + const fields = Object.keys(allFields).map((key) => { + return { + id: key, + label: key, + value: allFields[key], + } + }) + + setFields(fields) + } + }, [customOutput]) + + const handleUpdateField = (id) => (event) => { + const data = JSON.parse(customOutput) + + if (id === 'value.amount.decimal') { + data['value']['amount']['decimal'] = event.target.value + data['value']['amount']['atoms'] = (event.target.value * 1e11).toString() + } else if (id === 'total_supply.amount.decimal') { + data['total_supply']['amount']['decimal'] = event.target.value + data['total_supply']['amount']['atoms'] = (event.target.value * 1e11).toString() + } else if (id === 'token_ticker.string') { + data['token_ticker']['string'] = event.target.value + data['token_ticker']['hex'] = stringToHex(event.target.value) + } else if (id === 'metadata_uri.string') { + data['metadata_uri']['string'] = event.target.value + data['metadata_uri']['hex'] = stringToHex(event.target.value) + } else { + data[id] = event.target.value + } + + setCustomOutput(JSON.stringify(data, null, 2)) + } + + return ( +
+
Balance: {mlBalance} TML
+
+ Use template: + + +
+
+
+
+ {fields.map((field) => { + return ( +
+ + +
+ ) + })} +
+
+
+ +
+
+
{error &&
{error}
}
+
+ +
+
+
+ Transaction preview{' '} + +
+
+
{JSON.stringify(inputs, null, 2)}
+
>
+
+ {JSON.stringify(outputs, null, 2)} +
+
+
+
+ Fee:{' '} + {' '} + TML +
+
+
+
+ password: +
+ +
+
{transactionHex}
+
+
+ +
+
+ ) +} + +export default SendCustomOutput diff --git a/src/pages/SendCustomOutput/SendCustomOutput.module.css b/src/pages/SendCustomOutput/SendCustomOutput.module.css new file mode 100644 index 00000000..ae059b9f --- /dev/null +++ b/src/pages/SendCustomOutput/SendCustomOutput.module.css @@ -0,0 +1,166 @@ +.text { + padding-bottom: 20px; +} + +.text textarea { + width: 100%; + padding: 10px 20px; + height: 180px; + overflow: scroll; +} + +.augmentedData { + display: flex; + flex-direction: row; +} + +.augmentedData * { + overflow: scroll; +} + +.balance { + font-size: 14px; + font-weight: bold; +} + +.templateSelector { + margin: 10px 0; +} + +.templateSelector select { + padding: 2px 5px; + margin: 0 10px; +} + +.templateSelector button { + padding: 5px 10px; + margin: 0 10px; + background: rgb(var(--color-green)); + color: #fff; + border-radius: 4px; +} + +.workArea { + display: flex; + flex-direction: row; + gap: 40px; +} + +.workArea .form { + width: 50%; +} + +.workArea .text { + width: 50%; +} + +.workArea .text textarea { + font-family: monospace; + white-space: pre; + overflow-wrap: normal; + overflow-x: scroll; + resize: vertical; +} + +.workArea .fieldGroup { + margin: 10px 0; +} + +.workArea .fieldGroup label { + margin-right: 10px; + display: block; +} + +.workArea .fieldGroup .input { + margin-right: 10px; + width: 100%; +} + +.validationBox button { + padding: 10px; + background: rgb(var(--color-extra-light-gray)); + border-radius: 4px; + color: rgb(var(--color-green)); + margin: 10px 0; + cursor: pointer; +} + +.transactionPreview { + margin: 20px 0; +} + +.augmentedData { + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.augmentedData .input, .augmentedData .output { + width: 48%; + + font-family: monospace; + white-space: pre; + overflow-wrap: normal; + overflow-x: scroll; + resize: vertical; +} + +.transactionPreviewHeader { + font-size: 20px; + font-weight: bold; +} + +.transactionPreviewHeader button { + padding: 5px 10px; + background: rgb(var(--color-light-green)); + color: #fff; + border-radius: 4px; + margin-left: 10px; +} + +.fee { + font-size: 14px; + font-weight: bold; + margin: 20px 0; +} + +.transactionHexPreview { + +} + +.transactionHexPreviewInputs { + display: flex; + flex-direction: row; + gap: 20px; + align-items: center; +} + +.transactionHexPreviewInputs button { + padding: 5px 10px; + background: rgb(var(--color-light-green)); + color: #fff; + border-radius: 4px; +} + +.transactionHexPreviewResult { + margin: 20px 0; + padding: 5px 10px; + font-family: monospace; + border-radius: 5px; + word-break: break-all; + background: rgb(var(--color-light-gray)); +} + +.broadcast { + display: flex; + align-items: center; + justify-content: center; +} + +.broadcast button { + padding: 10px; + background: rgb(var(--color-green)); + color: #fff; + border-radius: 4px; + cursor: pointer; +} diff --git a/src/pages/index.js b/src/pages/index.js index c17b8dd7..2123d919 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -14,6 +14,7 @@ import CreateDelegationPage from './CreateDelegation/CreateDelegation' import DelegationStakePage from './DelegationStake/DelegationStake' import DelegationWithdrawPage from './DelegationWithdraw/DelegationWithdraw' import LockedBalancePage from './LockedBalance/LockedBalance' +import SendCustomOutput from './SendCustomOutput/SendCustomOutput' export { CreateAccountPage, @@ -31,5 +32,6 @@ export { CreateDelegationPage, DelegationStakePage, DelegationWithdrawPage, - LockedBalancePage + LockedBalancePage, + SendCustomOutput, } diff --git a/src/services/Crypto/Mintlayer/Mintlayer.js b/src/services/Crypto/Mintlayer/Mintlayer.js index 1695c3ed..c54d7ea1 100644 --- a/src/services/Crypto/Mintlayer/Mintlayer.js +++ b/src/services/Crypto/Mintlayer/Mintlayer.js @@ -22,6 +22,7 @@ import init, { SourceId, Amount, encode_output_token_transfer, + encode_output_issue_fungible_token, FreezableToken, TotalSupply, encode_output_issue_nft, } from './@mintlayerlib-js/wasm_wrappers.js' import { Mintlayer } from '@APIs' @@ -207,6 +208,124 @@ export const getOutputs = ({ ) } } +// +// function hexToUint8Array(hexString) { +// // Remove any spaces or non-hex characters (optional) +// hexString = hexString.replace(/[^0-9a-fA-F]/g, '') +// +// // Check for invalid length +// if (hexString.length % 2 !== 0) { +// throw new Error('Invalid hex string: length must be a multiple of 2') +// } +// +// const byteArray = new Uint8Array(hexString.length / 2) +// +// for (let i = 0; i < byteArray.length; i++) { +// const byteHex = hexString.substr(i * 2, 2) +// byteArray[i] = parseInt(byteHex, 16) +// } +// +// return byteArray +// } + +export const getOutputIssueFungibleToken = ({output, network, chainTip}) => { + const { + authority, + is_freezable, + metadata_uri, + number_of_decimals, + token_ticker, + total_supply, + } = output + + const _current_block_height = BigInt(Number(chainTip)) + + const is_token_freezable = is_freezable ? FreezableToken.Yes : FreezableToken.No + + const supply_amount = Amount.from_atoms(BigInt(total_supply.amount.atoms).toString()) + + const total_supply_type = TotalSupply[total_supply.type] + + console.log('authority,\n' + + ' token_ticker.hex,\n' + + ' metadata_uri.hex,\n' + + ' Number(number_of_decimals),\n' + + ' total_supply_type,\n' + + ' supply_amount,\n' + + ' is_token_freezable,\n' + + ' _current_block_height,\n' + + ' network,', authority, + Buffer.from(token_ticker.hex, 'hex'), + Buffer.from(metadata_uri.hex, 'hex'), + Number(number_of_decimals), + total_supply_type, + supply_amount, + is_token_freezable, + _current_block_height, + network,) + + try{ + encode_output_issue_fungible_token( + authority, + new Uint8Array([]), + new Uint8Array([]), + Number(number_of_decimals), + total_supply_type, + undefined, //supply_amount, + is_token_freezable, + _current_block_height, + network, + ) + } catch (e) { + console.error('Error in encode_output_issue_fungible_token', e) + } + + return encode_output_issue_fungible_token( + authority, + new Uint8Array([]), + new Uint8Array([]), + Number(number_of_decimals), + total_supply_type, + supply_amount, + is_token_freezable, + _current_block_height, + network, + ) +} + +export const getOutputIssueNft = ({output, network, chainTip}) => { + const { + authority, + token_ticker, + description, + } = output + + const _current_block_height = BigInt(Number(chainTip)) + + const token_id = new Uint8Array([]) + const name = new Uint8Array([]) + const ticker = token_ticker.string + const media_hash = new Uint8Array([]) + const creator = undefined + const media_uri = undefined + const icon_uri = undefined + const additional_metadata_uri = undefined + + return encode_output_issue_nft( + token_id, + authority, + name, + ticker, + description, + media_hash, + creator, + media_uri, + icon_uri, + additional_metadata_uri, + _current_block_height, + network, + ) +} export const getTransaction = (inputs, outputs) => { const flags = BigInt(0) diff --git a/src/utils/Helpers/ML/MLTransaction.js b/src/utils/Helpers/ML/MLTransaction.js index 5da6c8b7..569146d6 100644 --- a/src/utils/Helpers/ML/MLTransaction.js +++ b/src/utils/Helpers/ML/MLTransaction.js @@ -53,6 +53,7 @@ const getTransactionUtxos = ({ utxos, amount, tokenId }) => { for (let i = 0; i < utxos.length; i++) { lastIndex = i const utxoBalance = getUtxoBalance(utxos[i], tokenId) + console.log('balance < BigInt(amount)', balance < BigInt(amount), balance, BigInt(amount)) if (balance < BigInt(amount)) { balance += utxoBalance utxosToSpend.push(utxos[i]) @@ -186,7 +187,7 @@ const getArraySpead = (inputs) => { return inputsArray } -const totalUtxosAmount = (utxosToSpend, token) => { +export const totalUtxosAmount = (utxosToSpend, token) => { return utxosToSpend.reduce((acc, utxo) => { const requiredToken = token ? utxo.utxo.value.token_id === token @@ -297,6 +298,52 @@ const calculateTransactionSizeInBytes = async ({ return size } +const calculateCustomTransactionSizeInBytes = async ({ + network, + inputs, + outputs, + currentHeight, +}) => { + const requireUtxo = inputs + const addressList = getUtxoAddress(requireUtxo) + const transactionStrings = getUtxoTransactions(requireUtxo) + const transactionBytes = getTransactionsBytes(transactionStrings) + const outpointedSourceIds = await getOutpointedSourceIds(transactionBytes) + const inputsIds = await getTxInputs(outpointedSourceIds) + const inputsArray = getArraySpead(inputsIds) + + // make array from outputs with await + const outputsArrayItems = outputs.map((output) => { + if (output.type === 'Transfer') { + return ML.getOutputs({ + amount: BigInt(output.value.amount.atoms).toString(), + address: output.destination, + networkType: network, + }) + } + if (output.type === 'IssueFungibleToken') { + return ML.getOutputIssueFungibleToken({output, network, chainTip: currentHeight}) + } + if (output.type === 'IssueNft') { + return ML.getOutputIssueNft({output, network, chainTip: currentHeight}) + } + } + ) + + console.log('outputsArrayItems', outputsArrayItems) + + const outputsArray = getArraySpead(outputsArrayItems) + + const size = ML.getEstimatetransactionSize( + inputsArray, + addressList, + outputsArray, + network, + ) + + return size +} + const calculateSpenDelegFee = async ( address, amount, @@ -472,6 +519,59 @@ const sendTransaction = async ({ return JSON.parse(result).tx_id } +const sendCustomTransaction = async ({ + keysList, + network, + inputs, + outputs, + currentHeight, +}) => { + const requireUtxo = inputs + const transactionStrings = getUtxoTransactions(requireUtxo) + // const addressList = getUtxoAddress(requireUtxo) + const transactionBytes = getTransactionsBytes(transactionStrings) + const outpointedSourceIds = await getOutpointedSourceIds(transactionBytes) + const inputsIds = await getTxInputs(outpointedSourceIds) + const inputsArray = getArraySpead(inputsIds) + + // make array from outputs with await + const outputsArrayItems = outputs.map((output) => { + if (output.type === 'Transfer') { + return ML.getOutputs({ + amount: BigInt(output.value.amount.atoms).toString(), + address: output.destination, + networkType: network, + }) + } + if (output.type === 'IssueFungibleToken') { + return ML.getOutputIssueFungibleToken({ output, network, chainTip: currentHeight }) + } + } + ) + + const outputsArray = getArraySpead(outputsArrayItems) + + const transaction = await ML.getTransaction(inputsArray, outputsArray) + + const optUtxos = await getOptUtxos(requireUtxo, network) + + const encodedWitnesses = await getEncodedWitnesses( + requireUtxo, + keysList, + transaction, + optUtxos, // in fact that is transaction inputs + network, + ) + const finalWitnesses = getArraySpead(encodedWitnesses) + const encodedSignedTransaction = await ML.getEncodedSignedTransaction( + transaction, + finalWitnesses, + ) + const transactionHex = getTransactionHex(encodedSignedTransaction) + + return transactionHex +} + const spendFromDelegation = async ( keysList, address, @@ -578,6 +678,8 @@ export { getEncodedWitnesses, calculateSpenDelegFee, sendTransaction, + sendCustomTransaction, spendFromDelegation, calculateTransactionSizeInBytes, + calculateCustomTransactionSizeInBytes, }