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 (
+
+
+
+
+ )
+ })}
+
+
+
+
+
+
+
+
+
+
+
+
+ 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,
}