Skip to content

Commit

Permalink
feat: add signPartialTx api
Browse files Browse the repository at this point in the history
  • Loading branch information
AricRedemption committed Dec 24, 2024
1 parent 67d8ec1 commit 57457c6
Show file tree
Hide file tree
Showing 6 changed files with 197 additions and 1 deletion.
15 changes: 15 additions & 0 deletions src/content-script/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,21 @@ export async function pay(params: { transactions: SigningTransaction[] }) {
return await createAction('Pay', 'authorize', params)
}

export async function signPartialTx(params: {
transactions: SigningTransaction[]
utxos: {
txId: string
outputIndex: number
satoshis: number
address: string
height: number
}[]
signType?: number
hasMetaid?: boolean
}) {
return await createAction('SignPartialTx', 'authorize', params)
}

type TransferTask = {
genesis?: string
codehash?: string
Expand Down
3 changes: 3 additions & 0 deletions src/content-script/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
signTransaction,
signTransactions,
pay,
signPartialTx,
ecdh,
transferNFT,
omniConnect,
Expand Down Expand Up @@ -51,6 +52,7 @@ type Metalet = {
transfer: any
// transferAll: any
// merge: any
// getActivities: any

eciesEncrypt: any
eciesDecrypt: any
Expand Down Expand Up @@ -101,6 +103,7 @@ const metalet: any = {
signTransaction,
signTransactions,
pay,
signPartialTx,
signMessage,
verifySignature,

Expand Down
9 changes: 9 additions & 0 deletions src/data/authorize-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as EciesDecrypt from '../lib/actions/ecies-decrypt'
import * as SignTransaction from '../lib/actions/sign-transaction'
import * as SignTransactions from '../lib/actions/sign-transactions'
import * as Pay from '../lib/actions/pay'
import * as SignPartialTx from '../lib/actions/signPartialTx'
import * as SignMessage from '../lib/actions/sign-message'
import * as Merge from '../lib/actions/merge'

Expand Down Expand Up @@ -114,6 +115,14 @@ export default {
estimate: doNothing,
closeAfterProcess: true,
},
SignPartialTx: {
name: 'SignPartialTx',
title: 'Partial Transaction Signing',
description: ['Sign transactions with specified UTXOs'],
process: SignPartialTx.process,
estimate: doNothing,
closeAfterProcess: true,
},
SignBTCPsbt: {
name: 'SignBTCPsbt',
title: 'Sign Psbt',
Expand Down
10 changes: 10 additions & 0 deletions src/lib/actions/signPartialTx.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { payTransactionsWithUtxos } from '../crypto'

export async function process(params: any) {
const toPayTransactions = params.transactions
const utxos = params.utxos
const signType = params.signType || 0x01
const payedTransactions = await payTransactionsWithUtxos(toPayTransactions, utxos, signType, params.hasMetaid)

return { payedTransactions }
}
159 changes: 159 additions & 0 deletions src/lib/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,165 @@ export const payTransactions = async (
return payedTransactions
}

export const payTransactionsWithUtxos = async (
toPayTransactions: {
txComposer: string
message?: string
}[],
utxos: {
txId: string
outputIndex: number
satoshis: number
address: string
height: number
}[],
signType: number = mvc.crypto.Signature.SIGHASH_ALL,
hasMetaid: boolean = false
) => {
if (toPayTransactions.length !== utxos.length) {
throw new Error('The number of transactions must match the number of UTXOs')
}

const network = await getNetwork()
const wallet = await getCurrentWallet(Chain.MVC)
const activeWallet = await getActiveWalletOnlyAccount()
const password = await getPassword()
const address = wallet.getAddress()

const txids = new Map<string, string>()
toPayTransactions.forEach(({ txComposer: txComposerSerialized }) => {
const txid = TxComposer.deserialize(txComposerSerialized).getTxId()
txids.set(txid, txid)
})

const payedTransactions = []
for (let i = 0; i < toPayTransactions.length; i++) {
const toPayTransaction = toPayTransactions[i]
const currentUtxo = utxos[i]
const currentTxid = TxComposer.deserialize(toPayTransaction.txComposer).getTxId()

const txComposer = TxComposer.deserialize(toPayTransaction.txComposer)
const tx = txComposer.tx

const inputs = tx.inputs
const existingInputsLength = tx.inputs.length
for (let i = 0; i < inputs.length; i++) {
if (!inputs[i].output) {
throw new Error('The output of every input of the transaction must be provided')
}
}

if (hasMetaid) {
const { messages: metaIdMessages, outputIndex } = await parseLocalTransaction(tx)

if (outputIndex !== null) {
let replaceFound = false
const prevTxids = Array.from(txids.keys())

for (let i = 0; i < metaIdMessages.length; i++) {
for (let j = 0; j < prevTxids.length; j++) {
if (typeof metaIdMessages[i] !== 'string') continue

if (metaIdMessages[i].includes(prevTxids[j])) {
replaceFound = true
metaIdMessages[i] = (metaIdMessages[i] as string).replace(prevTxids[j], txids.get(prevTxids[j])!)
}
}
}

if (replaceFound) {
const opReturnOutput = new mvc.Transaction.Output({
script: mvc.Script.buildSafeDataOut(metaIdMessages),
satoshis: 0,
})
tx.outputs[outputIndex] = opReturnOutput
}
}
}

const addressObj = new mvc.Address(address, network)
const totalOutput = tx.outputs.reduce((acc, output) => acc + output.satoshis, 0)
const currentSize = tx.toBuffer().length + P2PKH_UNLOCK_SIZE
const currentFee = FEEB * currentSize
const totalRequired = totalOutput + currentFee

if (currentUtxo.satoshis < totalRequired) {
throw new Error(
`UTXO at index ${i} doesn't have enough balance. Required: ${totalRequired}, Available: ${currentUtxo.satoshis}`
)
}

txComposer.appendP2PKHInput({
address: addressObj,
txId: currentUtxo.txId,
outputIndex: currentUtxo.outputIndex,
satoshis: currentUtxo.satoshis,
})

const changeAmount = currentUtxo.satoshis - totalRequired
if (changeAmount > 0) {
txComposer.appendChangeOutput(addressObj, FEEB)
} else if (changeAmount < 0) {
throw new Error(
`UTXO at index ${i} is insufficient. Required: ${totalRequired}, Available: ${currentUtxo.satoshis}`
)
}

const mneObj = mvc.Mnemonic.fromString(decrypt(activeWallet.mnemonic, password))
const hdpk = mneObj.toHDPrivateKey('', network)

const rootPath = await getMvcRootPath()
const basePrivateKey = hdpk.deriveChild(rootPath)
const rootPrivateKey = mvc.PrivateKey.fromWIF(wallet.getPrivateKey())

const toUsePrivateKeys = new Map<number, mvc.PrivateKey>()
for (let i = 0; i < existingInputsLength; i++) {
const input = txComposer.getInput(i)
const prevTxId = input.prevTxId.toString('hex')
if (txids.has(prevTxId)) {
input.prevTxId = Buffer.from(txids.get(prevTxId)!, 'hex')
}

const inputAddress = mvc.Address.fromString(
input.output!.script.toAddress().toString(),
network === 'regtest' ? 'testnet' : network
).toString()
let deriver = 0
let toUsePrivateKey: mvc.PrivateKey | undefined = undefined
while (deriver < DERIVE_MAX_DEPTH) {
const childPk = basePrivateKey.deriveChild(0).deriveChild(deriver)
const childAddress = childPk.publicKey.toAddress(network === 'regtest' ? 'testnet' : network).toString()

if (childAddress === inputAddress.toString()) {
toUsePrivateKey = childPk.privateKey
break
}

deriver++
}

if (!toUsePrivateKey) {
throw new Error(`Cannot find the private key of index #${i} input`)
}

toUsePrivateKeys.set(i, toUsePrivateKey)
}

toUsePrivateKeys.forEach((privateKey, index) => {
txComposer.unlockP2PKHInput(privateKey, index, signType)
})

txComposer.unlockP2PKHInput(rootPrivateKey, existingInputsLength, signType)

const txid = txComposer.getTxId()
txids.set(currentTxid, txid)

payedTransactions.push(txComposer.serialize())
}

return payedTransactions
}

type SA_utxo = {
txId: string
outputIndex: number
Expand Down
2 changes: 1 addition & 1 deletion vite-config/vite.dev.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export default defineConfig({
],

server: {
port: 3000,
...(env.VITE_DEV_PORT && { port: Number(env.VITE_DEV_PORT) }),
open: true,
},
})

0 comments on commit 57457c6

Please sign in to comment.