Skip to content

Commit

Permalink
Fix offchain KMS signature id methods. (#135)
Browse files Browse the repository at this point in the history
  • Loading branch information
Hathoriel authored Sep 1, 2021
1 parent e07ddce commit 7cbb187
Show file tree
Hide file tree
Showing 16 changed files with 256 additions and 144 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tatumio/tatum",
"version": "1.23.6",
"version": "1.23.7",
"description": "Tatum API client allows browsers and Node.js clients to interact with Tatum API.",
"main": "dist/src/index.js",
"repository": "https://github.com/tatumio/tatum-js",
Expand Down
2 changes: 1 addition & 1 deletion src/model/request/TransferXlmOffchain.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {IsNotEmpty, Length} from 'class-validator'
import { IsNotEmpty, IsUUID, Length, ValidateIf } from 'class-validator'
import {CreateWithdrawal} from './CreateWithdrawal'

export class TransferXlmOffchain extends CreateWithdrawal {
Expand Down
58 changes: 29 additions & 29 deletions src/offchain/ada.spec.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,37 @@
import {sendAdaOffchainTransaction} from './ada';
import { sendAdaOffchainTransaction } from './ada';

describe('ADA offchain', () => {
it('should transaction with mnemonic and xpub', async () => {
try {
const tx = await sendAdaOffchainTransaction(true, {
senderAccountId: '60f990befd2f551040f512c0',
address: 'addr_test1qp33h99feurpn7n8cezqthh75723q5kjwqmthaf073y7edlg9xj6jj5qs9pe3nxq8rx59aa5qlmjrgsm0jt22hh3ll5q7n3j5s',
amount: '1',
xpub: '59bed551a66a043510e8f8cd7a0a34c630f82aeaa4198d212625b7c7438c82259dc3766148401d5a697cb7c8311b366258be6e74700419c3f385932d22fb49bf90ee448d740e6529d1936e33a1e806b1c745eba19d24e299757c9baed1caf24a5e25b1cd8da401a3ff37d4c6b8e4e984ffde03eb8cadd9bf3740152999604b04',
mnemonic: 'head surround recipe nuclear giraffe tool benefit steel plug obey damp scale suffer fortune lift tree affair oyster engine ceiling physical emotion drink bubble',
it('should transaction with mnemonic and xpub', async () => {
try {
const tx = await sendAdaOffchainTransaction(true, {
senderAccountId: '60f990befd2f551040f512c0',
address: 'addr_test1qp33h99feurpn7n8cezqthh75723q5kjwqmthaf073y7edlg9xj6jj5qs9pe3nxq8rx59aa5qlmjrgsm0jt22hh3ll5q7n3j5s',
amount: '1',
xpub: '59bed551a66a043510e8f8cd7a0a34c630f82aeaa4198d212625b7c7438c82259dc3766148401d5a697cb7c8311b366258be6e74700419c3f385932d22fb49bf90ee448d740e6529d1936e33a1e806b1c745eba19d24e299757c9baed1caf24a5e25b1cd8da401a3ff37d4c6b8e4e984ffde03eb8cadd9bf3740152999604b04',
mnemonic: 'head surround recipe nuclear giraffe tool benefit steel plug obey damp scale suffer fortune lift tree affair oyster engine ceiling physical emotion drink bubble',
})
expect(tx).not.toBeNull()
expect(tx).toHaveProperty('txId')
} catch (e) {
console.log(e)
}
})
expect(tx).not.toBeNull()
expect(tx).toHaveProperty('txId')
} catch (e) {
console.log(e)
}
})

it('should with keypair', async () => {
const tx = await sendAdaOffchainTransaction(true, {
keyPair: [
{
privateKey: '88e0687cf43333502b03eb27d857c413ac97305312d607b7599ab5a889007d55cb9693455af5f1813b9bd38f574dc6136181bf1781641c2525fd51556bfd1ac1df44389d8de89b9e74585c6d6d14b08922fcecd038f2def7e50f5488559be2abad9b25ef114471eab8f0f83cc7a2442665e2c453f01e9ec55ef07c1ebae98397',
address: 'addr_test1qz9se7lx6rkctnpx8y3hm68lqmq58kq5dycumj9d6e5qgchat2zasvwcf2ws75fr33qdlxg8g305wn57hhkujrya4a5q0k60f0',
},
],
senderAccountId: '60ec1b6879cba0127ceb73ab',
address: 'addr_test1qqr585tvlc7ylnqvz8pyqwauzrdu0mxag3m7q56grgmgu7sxu2hyfhlkwuxupa9d5085eunq2qywy7hvmvej456flknswgndm3',
amount: '1',
it('should with keypair', async () => {
const tx = await sendAdaOffchainTransaction(true, {
keyPair: [
{
privateKey: '88e0687cf43333502b03eb27d857c413ac97305312d607b7599ab5a889007d55cb9693455af5f1813b9bd38f574dc6136181bf1781641c2525fd51556bfd1ac1df44389d8de89b9e74585c6d6d14b08922fcecd038f2def7e50f5488559be2abad9b25ef114471eab8f0f83cc7a2442665e2c453f01e9ec55ef07c1ebae98397',
address: 'addr_test1qz9se7lx6rkctnpx8y3hm68lqmq58kq5dycumj9d6e5qgchat2zasvwcf2ws75fr33qdlxg8g305wn57hhkujrya4a5q0k60f0',
},
],
senderAccountId: '60ec1b6879cba0127ceb73ab',
address: 'addr_test1qqr585tvlc7ylnqvz8pyqwauzrdu0mxag3m7q56grgmgu7sxu2hyfhlkwuxupa9d5085eunq2qywy7hvmvej456flknswgndm3',
amount: '1',
})
expect(tx).not.toBeNull()
expect(tx).toHaveProperty('txId')
})
expect(tx).not.toBeNull()
expect(tx).toHaveProperty('txId')
})


})
243 changes: 131 additions & 112 deletions src/offchain/ada.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,26 @@
import {BigNum, hash_transaction, Transaction, TransactionBody, TransactionBuilder, TransactionWitnessSet, Vkeywitnesses,} from '@emurgo/cardano-serialization-lib-nodejs';
import {
BigNum,
hash_transaction,
Transaction,
TransactionBody,
TransactionBuilder,
TransactionWitnessSet,
Vkeywitnesses,
} from '@emurgo/cardano-serialization-lib-nodejs';
import BigNumber from 'bignumber.js';
import {validateBody} from '../connector/tatum';
import {Currency, KeyPair, TransactionKMS, TransferBtcBasedOffchain, WithdrawalResponseData} from '../model';
import {adaToLovelace, addAddressInputsWithoutPrivateKey, addInput, addOutputAda, initTransactionBuilder, makeWitness,} from '../transaction';
import {generateAddressFromXPub, generatePrivateKeyFromMnemonic} from '../wallet';
import {offchainBroadcast, offchainCancelWithdrawal, offchainStoreWithdrawal} from './common';
import { validateBody } from '../connector/tatum';
import { Currency, KeyPair, TransactionKMS, TransferBtcBasedOffchain, WithdrawalResponseData } from '../model';
import {
adaToLovelace,
addAddressInputsWithoutPrivateKey,
addInput,
addOutputAda,
initTransactionBuilder,
makeWitness,
} from '../transaction';
import { generateAddressFromXPub, generatePrivateKeyFromMnemonic } from '../wallet';
import { offchainBroadcast, offchainCancelWithdrawal, offchainStoreWithdrawal } from './common';
import { offchainTransferAdaKMS } from './kms'

/**
* Send Ada transaction from Tatum Ledger account to the blockchain. This method broadcasts signed transaction to the blockchain.
Expand All @@ -14,114 +30,117 @@ import {offchainBroadcast, offchainCancelWithdrawal, offchainStoreWithdrawal} fr
* @returns transaction id of the transaction in the blockchain or id of the withdrawal, if it was not cancelled automatically
*/
export const sendAdaOffchainTransaction = async (testnet: boolean, body: TransferBtcBasedOffchain) => {
await validateBody(body, TransferBtcBasedOffchain);
const {
mnemonic, keyPair, xpub, attr: changeAddress, ...withdrawal
} = body
if (!withdrawal.fee) {
withdrawal.fee = '1'
}
const { id, data } = await offchainStoreWithdrawal(withdrawal)
const {
amount, address,
} = withdrawal
let txData
try {
txData = await prepareAdaSignedOffchainTransaction(testnet, data, amount, address, mnemonic, keyPair, changeAddress, xpub, withdrawal.multipleAmounts)
} catch (e) {
console.error(e)
await offchainCancelWithdrawal(id)
throw e
}
try {
return { ...await offchainBroadcast({ txData, withdrawalId: id, currency: Currency.ADA }), id }
} catch (e) {
console.error(e)
if (body.signatureId) {
return offchainTransferAdaKMS(body)
}
await validateBody(body, TransferBtcBasedOffchain);
const {
mnemonic, keyPair, xpub, attr: changeAddress, ...withdrawal
} = body
if (!withdrawal.fee) {
withdrawal.fee = '1'
}
const { id, data } = await offchainStoreWithdrawal(withdrawal)
const {
amount, address,
} = withdrawal
let txData
try {
await offchainCancelWithdrawal(id)
} catch (e1) {
console.log(e)
return { id }
txData = await prepareAdaSignedOffchainTransaction(testnet, data, amount, address, mnemonic, keyPair, changeAddress, xpub, withdrawal.multipleAmounts)
} catch (e) {
console.error(e)
await offchainCancelWithdrawal(id)
throw e
}
try {
return { ...await offchainBroadcast({ txData, withdrawalId: id, currency: Currency.ADA }), id }
} catch (e) {
console.error(e)
try {
await offchainCancelWithdrawal(id)
} catch (e1) {
console.log(e)
return { id }
}
throw e
}
throw e
}
}

const prepareAdaSignedOffchainTransaction = async (testnet: boolean, data: WithdrawalResponseData[], amount: string, address: string, mnemonic?: string, keyPair?: KeyPair[],
changeAddress?: string, xpub?: string, multipleAmounts?: string[], signatureId?: string) => {
const txBuilder = await initTransactionBuilder()
const fromAddress = data.filter(input => input.address).map(input => ({ address: input.address.address }))
await addAddressInputsWithoutPrivateKey(txBuilder, fromAddress)
const txBuilder = await initTransactionBuilder()
const fromAddress = data.filter(input => input.address).map(input => ({ address: input.address.address }))
await addAddressInputsWithoutPrivateKey(txBuilder, fromAddress)

addOffchainInputs(txBuilder, data)
if (multipleAmounts?.length) {
for (const [i, multipleAmount] of multipleAmounts.entries()) {
addOutputAda(txBuilder, address.split(',')[i], multipleAmount)
addOffchainInputs(txBuilder, data)
if (multipleAmounts?.length) {
for (const [i, multipleAmount] of multipleAmounts.entries()) {
addOutputAda(txBuilder, address.split(',')[i], multipleAmount)
}
} else {
addOutputAda(txBuilder, address, amount)
}
} else {
addOutputAda(txBuilder, address, amount)
}

const lastVin = data.find(d => d.vIn === '-1') as WithdrawalResponseData
if (new BigNumber(lastVin.amount).isGreaterThan(0)) {
if (xpub) {
const zeroAddress = await generateAddressFromXPub(Currency.ADA, testnet, xpub, 0)
addOutputAda(txBuilder, zeroAddress, lastVin.amount)
} else if (changeAddress) {
addOutputAda(txBuilder, changeAddress, lastVin.amount)
} else {
throw new Error('Impossible to prepare transaction. Either xpub or keyPair and attr must be present.')
const lastVin = data.find(d => d.vIn === '-1') as WithdrawalResponseData
if (new BigNumber(lastVin.amount).isGreaterThan(0)) {
if (xpub) {
const zeroAddress = await generateAddressFromXPub(Currency.ADA, testnet, xpub, 0)
addOutputAda(txBuilder, zeroAddress, lastVin.amount)
} else if (changeAddress) {
addOutputAda(txBuilder, changeAddress, lastVin.amount)
} else {
throw new Error('Impossible to prepare transaction. Either xpub or keyPair and attr must be present.')
}
}
}

const lovelaceFee = adaToLovelace(1)
txBuilder.set_fee(BigNum.from_str(lovelaceFee))
const lovelaceFee = adaToLovelace(1)
txBuilder.set_fee(BigNum.from_str(lovelaceFee))

const txBody = txBuilder.build()
if (signatureId) {
return JSON.stringify({ txData: txBody.to_bytes().toString() })
}
const vKeyWitnesses = Vkeywitnesses.new()
const txHash = hash_transaction(txBody)
for (const input of data) {
// when there is no address field present, input is pool transfer to 0
if (input.vIn === '-1') {
continue
const txBody = txBuilder.build()
if (signatureId) {
return JSON.stringify({ txData: txBody.to_bytes().toString() })
}
const vKeyWitnesses = Vkeywitnesses.new()
const txHash = hash_transaction(txBody)
for (const input of data) {
// when there is no address field present, input is pool transfer to 0
if (input.vIn === '-1') {
continue
}

if (mnemonic) {
const derivationKey = input.address?.derivationKey || 0
const privateKey = await generatePrivateKeyFromMnemonic(Currency.ADA, testnet, mnemonic, derivationKey)
makeWitness(privateKey, txHash, vKeyWitnesses)
} else if (keyPair) {
const { privateKey } = keyPair.find(k => k.address === input.address.address) as KeyPair
makeWitness(privateKey, txHash, vKeyWitnesses)
} else {
throw new Error('Impossible to prepare transaction. Either mnemonic or keyPair and attr must be present.')
}
if (mnemonic) {
const derivationKey = input.address?.derivationKey || 0
const privateKey = await generatePrivateKeyFromMnemonic(Currency.ADA, testnet, mnemonic, derivationKey)
makeWitness(privateKey, txHash, vKeyWitnesses)
} else if (keyPair) {
const { privateKey } = keyPair.find(k => k.address === input.address.address) as KeyPair
makeWitness(privateKey, txHash, vKeyWitnesses)
} else {
throw new Error('Impossible to prepare transaction. Either mnemonic or keyPair and attr must be present.')
}


}
const witnesses = TransactionWitnessSet.new()
witnesses.set_vkeys(vKeyWitnesses)
return Buffer.from(
Transaction.new(txBody, witnesses).to_bytes(),
).toString('hex')
}
const witnesses = TransactionWitnessSet.new()
witnesses.set_vkeys(vKeyWitnesses)
return Buffer.from(
Transaction.new(txBody, witnesses).to_bytes(),
).toString('hex')
}

const addOffchainInputs = (transactionBuilder: TransactionBuilder, inputs: WithdrawalResponseData[]) => {
let amount = new BigNumber(0)
for (const input of inputs) {
if (input.vIn !== '-1' && input.amount && input.vInIndex !== undefined && input.address?.address) {
addInput(transactionBuilder, {
value: adaToLovelace(input.amount),
index: input.vInIndex,
txHash: input.vIn,
}, input.address.address)
amount = amount.plus(input.amount)
let amount = new BigNumber(0)
for (const input of inputs) {
if (input.vIn !== '-1' && input.amount && input.vInIndex !== undefined && input.address?.address) {
addInput(transactionBuilder, {
value: adaToLovelace(input.amount),
index: input.vInIndex,
txHash: input.vIn,
}, input.address.address)
amount = amount.plus(input.amount)
}
}
}
return amount
return amount
}

/**
Expand All @@ -132,24 +151,24 @@ const addOffchainInputs = (transactionBuilder: TransactionBuilder, inputs: Withd
* @returns transaction data to be broadcast to blockchain.
*/
export const signAdaOffchainKMSTransaction = async (tx: TransactionKMS, mnemonic: string, testnet: boolean) => {
if (tx.chain !== Currency.ADA || !tx.withdrawalResponses) {
throw Error('Unsupported chain.')
}
const txData = JSON.parse(tx.serializedTransaction).txData
const transactionBody = TransactionBody.from_bytes(Uint8Array.from(txData.split(',')))
const txHash = hash_transaction(transactionBody)
const vKeyWitnesses = Vkeywitnesses.new()
for (const response of tx.withdrawalResponses) {
if (response.vIn === '-1') {
continue
if (tx.chain !== Currency.ADA || !tx.withdrawalResponses) {
throw Error('Unsupported chain.')
}
const txData = JSON.parse(tx.serializedTransaction).txData
const transactionBody = TransactionBody.from_bytes(Uint8Array.from(txData.split(',')))
const txHash = hash_transaction(transactionBody)
const vKeyWitnesses = Vkeywitnesses.new()
for (const response of tx.withdrawalResponses) {
if (response.vIn === '-1') {
continue
}
const privateKey = await generatePrivateKeyFromMnemonic(Currency.ADA, testnet, mnemonic, response.address?.derivationKey || 0)
makeWitness(privateKey, txHash, vKeyWitnesses)
}
const privateKey = await generatePrivateKeyFromMnemonic(Currency.ADA, testnet, mnemonic, response.address?.derivationKey || 0)
makeWitness(privateKey, txHash, vKeyWitnesses)
}
const witnesses = TransactionWitnessSet.new()
witnesses.set_vkeys(vKeyWitnesses)
return Buffer.from(
Transaction.new(transactionBody, witnesses).to_bytes(),
).toString('hex')
const witnesses = TransactionWitnessSet.new()
witnesses.set_vkeys(vKeyWitnesses)
return Buffer.from(
Transaction.new(transactionBody, witnesses).to_bytes(),
).toString('hex')

}
4 changes: 4 additions & 0 deletions src/offchain/bcash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {validateBody} from '../connector/tatum'
import {Currency, KeyPair, TransactionKMS, TransferBtcBasedOffchain, WithdrawalResponseData} from '../model'
import {generateAddressFromXPub, generateBchWallet, generatePrivateKeyFromMnemonic, toLegacyAddress} from '../wallet'
import {offchainBroadcast, offchainCancelWithdrawal, offchainStoreWithdrawal} from './common'
import { offchainTransferBcashKMS } from './kms'
// tslint:disable-next-line:no-var-requires
const bcash = require('@tatumio/bitcoincashjs2-lib')

Expand All @@ -16,6 +17,9 @@ const bcash = require('@tatumio/bitcoincashjs2-lib')
* @returns transaction id of the transaction in the blockchain or id of the withdrawal, if it was not cancelled automatically
*/
export const sendBitcoinCashOffchainTransaction = async (testnet: boolean, body: TransferBtcBasedOffchain) => {
if(body.signatureId) {
return offchainTransferBcashKMS(body)
}
await validateBody(body, TransferBtcBasedOffchain)
const {
mnemonic, keyPair, attr: changeAddress, ...withdrawal
Expand Down
4 changes: 4 additions & 0 deletions src/offchain/bitcoin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {validateBody} from '../connector/tatum'
import {Currency, KeyPair, TransactionKMS, TransferBtcBasedOffchain, WithdrawalResponseData} from '../model'
import {generateAddressFromXPub, generatePrivateKeyFromMnemonic} from '../wallet'
import {offchainBroadcast, offchainCancelWithdrawal, offchainStoreWithdrawal} from './common'
import { offchainTransferBtcKMS } from './kms'

/**
* Send Bitcoin transaction from Tatum Ledger account to the blockchain. This method broadcasts signed transaction to the blockchain.
Expand All @@ -14,6 +15,9 @@ import {offchainBroadcast, offchainCancelWithdrawal, offchainStoreWithdrawal} fr
* @returns transaction id of the transaction in the blockchain or id of the withdrawal, if it was not cancelled automatically
*/
export const sendBitcoinOffchainTransaction = async (testnet: boolean, body: TransferBtcBasedOffchain) => {
if (body.signatureId) {
return offchainTransferBtcKMS(body)
}
await validateBody(body, TransferBtcBasedOffchain)
const {
mnemonic, keyPair, xpub, attr: changeAddress, ...withdrawal
Expand Down
Loading

0 comments on commit 7cbb187

Please sign in to comment.