diff --git a/.changeset/sweet-garlics-greet.md b/.changeset/sweet-garlics-greet.md new file mode 100644 index 0000000..56dc5cc --- /dev/null +++ b/.changeset/sweet-garlics-greet.md @@ -0,0 +1,9 @@ +--- +'@rosen-chains/abstract-chain': major +'@rosen-chains/bitcoin': minor +'@rosen-chains/cardano': minor +'@rosen-chains/ergo': minor +'@rosen-chains/evm': minor +--- + +add verifyPaymentTransaction function which checks data consistency within a PaymentTransaction Object diff --git a/packages/abstract-chain/lib/AbstractChain.ts b/packages/abstract-chain/lib/AbstractChain.ts index 5c24fca..9e89532 100644 --- a/packages/abstract-chain/lib/AbstractChain.ts +++ b/packages/abstract-chain/lib/AbstractChain.ts @@ -474,6 +474,15 @@ abstract class AbstractChain { * serializes the transaction of this chain into string */ protected abstract serializeTx: (tx: TxType) => string; + + /** + * verifies consistency within the PaymentTransaction object + * @param transaction the PaymentTransaction + * @returns true if the transaction is verified + */ + abstract verifyPaymentTransaction: ( + transaction: PaymentTransaction + ) => Promise; } export default AbstractChain; diff --git a/packages/abstract-chain/tests/TestChain.ts b/packages/abstract-chain/tests/TestChain.ts index 0758fbb..c38e138 100644 --- a/packages/abstract-chain/tests/TestChain.ts +++ b/packages/abstract-chain/tests/TestChain.ts @@ -26,6 +26,7 @@ class TestChain extends AbstractChain { getMinimumNativeToken = this.notImplemented; PaymentTransactionFromJson = this.notImplemented; rawTxToPaymentTransaction = this.notImplemented; + verifyPaymentTransaction = this.notImplemented; generateMultipleTransactions = ( eventId: string, diff --git a/packages/abstract-chain/tests/TestUtxoChain.ts b/packages/abstract-chain/tests/TestUtxoChain.ts index 7689507..42c2175 100644 --- a/packages/abstract-chain/tests/TestUtxoChain.ts +++ b/packages/abstract-chain/tests/TestUtxoChain.ts @@ -25,6 +25,7 @@ class TestUtxoChain extends AbstractUtxoChain { getMinimumNativeToken = this.notImplemented; PaymentTransactionFromJson = this.notImplemented; rawTxToPaymentTransaction = this.notImplemented; + verifyPaymentTransaction = this.notImplemented; getBoxInfo = (box: string): BoxInfo => { throw Error('Not mocked'); diff --git a/packages/chains/bitcoin/lib/BitcoinChain.ts b/packages/chains/bitcoin/lib/BitcoinChain.ts index 0638b20..192ec0f 100644 --- a/packages/chains/bitcoin/lib/BitcoinChain.ts +++ b/packages/chains/bitcoin/lib/BitcoinChain.ts @@ -725,6 +725,56 @@ class BitcoinChain extends AbstractUtxoChain { */ protected unwrapBtc = (amount: bigint): RosenAmount => this.tokenMap.unwrapAmount(this.NATIVE_TOKEN_ID, amount, this.CHAIN); + + /** + * verifies consistency within the PaymentTransaction object + * @param transaction the PaymentTransaction + * @returns true if the transaction is verified + */ + verifyPaymentTransaction = async ( + transaction: PaymentTransaction + ): Promise => { + const psbt = Serializer.deserialize(transaction.txBytes); + const bitcoinTx = transaction as BitcoinTransaction; + const baseError = `Tx [${transaction.txId}] is not verified: `; + + // verify txId + const txId = Transaction.fromBuffer(psbt.data.getTransaction()).getId(); + if (transaction.txId !== txId) { + this.logger.warn( + baseError + + `Transaction id is inconsistent (expected [${transaction.txId}] found [${txId}])` + ); + return false; + } + + // verify inputUtxos + if (bitcoinTx.inputUtxos.length !== psbt.inputCount) { + this.logger.warn( + baseError + + `BitcoinTransaction object input counts is inconsistent [${bitcoinTx.inputUtxos.length} != ${psbt.inputCount}]` + ); + return false; + } + for (let i = 0; i < psbt.inputCount; i++) { + const input = psbt.txInputs[i]; + const txId = Buffer.from(input.hash).reverse().toString('hex'); + const actualInputId = `${txId}.${input.index}`; + const bitcoinInput = JsonBigInt.parse( + bitcoinTx.inputUtxos[i] + ) as BitcoinUtxo; + const expectedId = `${bitcoinInput.txId}.${bitcoinInput.index}`; + if (expectedId !== actualInputId) { + this.logger.warn( + baseError + + `Utxo id for input at index [${i}] is inconsistent [expected ${expectedId} found ${actualInputId}]` + ); + return false; + } + } + + return true; + }; } export default BitcoinChain; diff --git a/packages/chains/bitcoin/tests/BitcoinChain.spec.ts b/packages/chains/bitcoin/tests/BitcoinChain.spec.ts index 4028c58..84cf85d 100644 --- a/packages/chains/bitcoin/tests/BitcoinChain.spec.ts +++ b/packages/chains/bitcoin/tests/BitcoinChain.spec.ts @@ -719,7 +719,7 @@ describe('BitcoinChain', () => { const network = new TestBitcoinNetwork(); /** - * @target BitcoinChain.getBoxInfo should get box id and assets correctly + * @target BitcoinChain.getBoxInfo should get box info successfully * @dependencies * @scenario * - mock a BitcoinUtxo with assets @@ -830,4 +830,114 @@ describe('BitcoinChain', () => { expect(result).toEqual(trackMap); }); }); + + describe('verifyPaymentTransaction', () => { + const network = new TestBitcoinNetwork(); + + /** + * @target BitcoinChain.verifyPaymentTransaction should return true + * when data is consistent + * @dependencies + * @scenario + * - mock a BitcoinTransaction + * - run test + * - check returned value + * @expected + * - it should return true + */ + it('should return true when data is consistent', async () => { + // mock a BitcoinTransaction + const paymentTx = BitcoinTransaction.fromJson( + testData.transaction2PaymentTransaction + ); + + // run test + const bitcoinChain = testUtils.generateChainObject(network); + const result = await bitcoinChain.verifyPaymentTransaction(paymentTx); + + // check returned value + expect(result).toEqual(true); + }); + + /** + * @target BitcoinChain.verifyPaymentTransaction should return false + * when transaction id is wrong + * @dependencies + * @scenario + * - mock a BitcoinTransaction with changed txId + * - run test + * - check returned value + * @expected + * - it should return false + */ + it('should return false when transaction id is wrong', async () => { + // mock a BitcoinTransaction with changed txId + const paymentTx = BitcoinTransaction.fromJson( + testData.transaction2PaymentTransaction + ); + paymentTx.txId = testUtils.generateRandomId(); + + // run test + const bitcoinChain = testUtils.generateChainObject(network); + const result = await bitcoinChain.verifyPaymentTransaction(paymentTx); + + // check returned value + expect(result).toEqual(false); + }); + + /** + * @target BitcoinChain.verifyPaymentTransaction should return false + * when number of utxos is wrong + * @dependencies + * @scenario + * - mock a BitcoinTransaction with less utxos + * - run test + * - check returned value + * @expected + * - it should return false + */ + it('should return false when number of utxos is wrong', async () => { + // mock a BitcoinTransaction with less utxos + const paymentTx = BitcoinTransaction.fromJson( + testData.transaction2PaymentTransaction + ); + paymentTx.inputUtxos.pop(); + + // run test + const bitcoinChain = testUtils.generateChainObject(network); + const result = await bitcoinChain.verifyPaymentTransaction(paymentTx); + + // check returned value + expect(result).toEqual(false); + }); + + /** + * @target BitcoinChain.verifyPaymentTransaction should return false + * when at least one of the utxos is wrong + * @dependencies + * @scenario + * - mock a BitcoinTransaction with changed utxo + * - run test + * - check returned value + * @expected + * - it should return false + */ + it('should return false when at least one of the utxos is wrong', async () => { + // mock a BitcoinTransaction with changed utxo + const paymentTx = BitcoinTransaction.fromJson( + testData.transaction2PaymentTransaction + ); + paymentTx.inputUtxos[1] = JsonBigInt.stringify({ + txId: testUtils.generateRandomId(), + index: 1, + }); + + // run test + const bitcoinChain = testUtils.generateChainObject(network); + const result = await bitcoinChain.verifyPaymentTransaction(paymentTx); + + // check returned value + expect(result).toEqual(false); + }); + }); }); diff --git a/packages/chains/cardano/lib/CardanoChain.ts b/packages/chains/cardano/lib/CardanoChain.ts index 8e680a2..933d54f 100644 --- a/packages/chains/cardano/lib/CardanoChain.ts +++ b/packages/chains/cardano/lib/CardanoChain.ts @@ -783,6 +783,62 @@ class CardanoChain extends AbstractUtxoChain { * serializes the transaction of this chain into string */ protected serializeTx = (tx: CardanoTx): string => JsonBigInt.stringify(tx); + + /** + * verifies consistency within the PaymentTransaction object + * @param transaction the PaymentTransaction + * @returns true if the transaction is verified + */ + verifyPaymentTransaction = async ( + transaction: PaymentTransaction + ): Promise => { + const tx = Serializer.deserialize(transaction.txBytes); + const cardanoTx = transaction as CardanoTransaction; + const baseError = `Tx [${transaction.txId}] is not verified: `; + + // verify txId + const txId = Buffer.from( + CardanoWasm.hash_transaction(tx.body()).to_bytes() + ).toString('hex'); + if (transaction.txId !== txId) { + this.logger.warn( + baseError + + `Transaction ID is inconsistent (expected [${transaction.txId}] found [${txId}])` + ); + return false; + } + + // verify inputUtxos + const txInputs = tx.body().inputs(); + if (cardanoTx.inputUtxos.length !== txInputs.len()) { + this.logger.warn( + baseError + + `CardanoTransaction object input counts is inconsistent [${ + cardanoTx.inputUtxos.length + } != ${txInputs.len()}]` + ); + return false; + } + for (let i = 0; i < txInputs.len(); i++) { + const input = txInputs.get(i); + const actualInputId = `${input + .transaction_id() + .to_hex()}.${input.index()}`; + const cardanoInput = JsonBigInt.parse( + cardanoTx.inputUtxos[i] + ) as CardanoUtxo; + const expectedId = `${cardanoInput.txId}.${cardanoInput.index}`; + if (expectedId !== actualInputId) { + this.logger.warn( + baseError + + `BoxId for input at index [${i}] is inconsistent [expected ${expectedId} found ${actualInputId}]` + ); + return false; + } + } + + return true; + }; } export default CardanoChain; diff --git a/packages/chains/cardano/tests/CardanoChain.spec.ts b/packages/chains/cardano/tests/CardanoChain.spec.ts index 43287bf..5c6eb17 100644 --- a/packages/chains/cardano/tests/CardanoChain.spec.ts +++ b/packages/chains/cardano/tests/CardanoChain.spec.ts @@ -1233,4 +1233,114 @@ describe('CardanoChain', () => { expect(result.toJson()).toEqual(expectedTx.toJson()); }); }); + + describe('verifyPaymentTransaction', () => { + const network = new TestCardanoNetwork(); + + /** + * @target CardanoChain.verifyPaymentTransaction should return true + * when data is consistent + * @dependencies + * @scenario + * - mock a CardanoTransaction + * - run test + * - check returned value + * @expected + * - it should return true + */ + it('should return true when data is consistent', async () => { + // mock a CardanoTransaction + const paymentTx = CardanoTransaction.fromJson( + TestData.transaction5PaymentTransaction + ); + + // run test + const cardanoChain = TestUtils.generateChainObject(network); + const result = await cardanoChain.verifyPaymentTransaction(paymentTx); + + // check returned value + expect(result).toEqual(true); + }); + + /** + * @target CardanoChain.verifyPaymentTransaction should return false + * when transaction id is wrong + * @dependencies + * @scenario + * - mock a CardanoTransaction with changed txId + * - run test + * - check returned value + * @expected + * - it should return false + */ + it('should return false when transaction id is wrong', async () => { + // mock a CardanoTransaction with changed txId + const paymentTx = CardanoTransaction.fromJson( + TestData.transaction5PaymentTransaction + ); + paymentTx.txId = TestUtils.generateRandomId(); + + // run test + const cardanoChain = TestUtils.generateChainObject(network); + const result = await cardanoChain.verifyPaymentTransaction(paymentTx); + + // check returned value + expect(result).toEqual(false); + }); + + /** + * @target CardanoChain.verifyPaymentTransaction should return false + * when number of boxes is wrong + * @dependencies + * @scenario + * - mock a CardanoTransaction with less boxes + * - run test + * - check returned value + * @expected + * - it should return false + */ + it('should return false when number of boxes is wrong', async () => { + // mock a CardanoTransaction with less boxes + const paymentTx = CardanoTransaction.fromJson( + TestData.transaction5PaymentTransaction + ); + paymentTx.inputUtxos.pop(); + + // run test + const cardanoChain = TestUtils.generateChainObject(network); + const result = await cardanoChain.verifyPaymentTransaction(paymentTx); + + // check returned value + expect(result).toEqual(false); + }); + + /** + * @target CardanoChain.verifyPaymentTransaction should return false + * when at least one of the boxes is wrong + * @dependencies + * @scenario + * - mock a CardanoTransaction with changed box + * - run test + * - check returned value + * @expected + * - it should return false + */ + it('should return false when at least one of the boxes is wrong', async () => { + // mock a CardanoTransaction with changed box + const paymentTx = CardanoTransaction.fromJson( + TestData.transaction5PaymentTransaction + ); + paymentTx.inputUtxos[1] = JsonBigInt.stringify({ + txId: TestUtils.generateRandomId(), + index: 1, + }); + + // run test + const cardanoChain = TestUtils.generateChainObject(network); + const result = await cardanoChain.verifyPaymentTransaction(paymentTx); + + // check returned value + expect(result).toEqual(false); + }); + }); }); diff --git a/packages/chains/cardano/tests/testData.ts b/packages/chains/cardano/tests/testData.ts index 9d30b52..c638184 100644 --- a/packages/chains/cardano/tests/testData.ts +++ b/packages/chains/cardano/tests/testData.ts @@ -638,14 +638,14 @@ export const transaction5PaymentTransaction = ` "txId": "ebdaf0a3fe875a3f98d007ec646891bd6085ff5e447dd236890d524f55b53af5", "txType": "payment", "inputUtxos": [ - "{\\"txId\\":\\"756fcffd0cc9517e9b93603de0a802cd124ecf85e7953ecbd2efa2736335294b\\",\\"index\\":0,\\"value\\":1180940,\\"assets\\":[{\\"policy_id\\":\\"fca58ef8ba9ef1961e132b611de2f8abcd2f34831e615a6f80c5bb48\\",\\"asset_name\\":\\"777252534e2d6c6f656e\\",\\"quantity\\":7952200}]}", - "{\\"txId\\":\\"1b4ca08ad9b4441abec1509324b184305c0b0c0dda92091a806deacaee3585fa\\",\\"index\\":0,\\"value\\":1180940,\\"assets\\":[{\\"policy_id\\":\\"fca58ef8ba9ef1961e132b611de2f8abcd2f34831e615a6f80c5bb48\\",\\"asset_name\\":\\"777252534e2d6c6f656e\\",\\"quantity\\":6224894}]}", - "{\\"txId\\":\\"15e590b8310eda5fbe7ca741e3c2bd8f0b2961f1dc4c5251cd43d5b85cadc17f\\",\\"index\\":0,\\"value\\":1198180,\\"assets\\":[{\\"policy_id\\":\\"fca58ef8ba9ef1961e132b611de2f8abcd2f34831e615a6f80c5bb48\\",\\"asset_name\\":\\"77724552472d6c6f656e\\",\\"quantity\\":4306918719}]}", - "{\\"txId\\":\\"121e53cbe92d16fb5927f430502bcd0be4f44982b68725e0030f0250ec010de1\\",\\"index\\":0,\\"value\\":13364722,\\"assets\\":[]}", - "{\\"txId\\":\\"0b61ef396b9b22421768445f2f27352366e3736514a3a5977fb678dddbdb83e8\\",\\"index\\":0,\\"value\\":1198180,\\"assets\\":[{\\"policy_id\\":\\"fca58ef8ba9ef1961e132b611de2f8abcd2f34831e615a6f80c5bb48\\",\\"asset_name\\":\\"77724552472d6c6f656e\\",\\"quantity\\":4966797511}]}", - "{\\"txId\\":\\"ed299711c6fe5b903eb6f43a5bc7d9c4558c69075d24e9ad2c85e911570d8382\\",\\"index\\":0,\\"value\\":1180940,\\"assets\\":[{\\"policy_id\\":\\"fca58ef8ba9ef1961e132b611de2f8abcd2f34831e615a6f80c5bb48\\",\\"asset_name\\":\\"777252534e2d6c6f656e\\",\\"quantity\\":4026628}]}", - "{\\"txId\\":\\"01137a617c09a36ed22334e7ed8c9b94eb222eb72dc148850e1425d8518c60bd\\",\\"index\\":0,\\"value\\":1180940,\\"assets\\":[{\\"policy_id\\":\\"fca58ef8ba9ef1961e132b611de2f8abcd2f34831e615a6f80c5bb48\\",\\"asset_name\\":\\"546f6b656e2d6c6f656e\\",\\"quantity\\":261493}]}", - "{\\"txId\\":\\"7d729590af3934b4d4894111303bbc215d6dc5bce3ea2feb6ed1e7e7a8a271d9\\",\\"index\\":0,\\"value\\":24232705,\\"assets\\":[]}" + "{\\"txId\\":\\"01137a617c09a36ed22334e7ed8c9b94eb222eb72dc148850e1425d8518c60bd\\",\\"index\\":0,\\"value\\":1180940,\\"assets\\":[{\\"policy_id\\":\\"fca58ef8ba9ef1961e132b611de2f8abcd2f34831e615a6f80c5bb48\\",\\"asset_name\\":\\"777252534e2d6c6f656e\\",\\"quantity\\":7952200}]}", + "{\\"txId\\":\\"0b61ef396b9b22421768445f2f27352366e3736514a3a5977fb678dddbdb83e8\\",\\"index\\":0,\\"value\\":1180940,\\"assets\\":[{\\"policy_id\\":\\"fca58ef8ba9ef1961e132b611de2f8abcd2f34831e615a6f80c5bb48\\",\\"asset_name\\":\\"777252534e2d6c6f656e\\",\\"quantity\\":6224894}]}", + "{\\"txId\\":\\"121e53cbe92d16fb5927f430502bcd0be4f44982b68725e0030f0250ec010de1\\",\\"index\\":0,\\"value\\":1198180,\\"assets\\":[{\\"policy_id\\":\\"fca58ef8ba9ef1961e132b611de2f8abcd2f34831e615a6f80c5bb48\\",\\"asset_name\\":\\"77724552472d6c6f656e\\",\\"quantity\\":4306918719}]}", + "{\\"txId\\":\\"15e590b8310eda5fbe7ca741e3c2bd8f0b2961f1dc4c5251cd43d5b85cadc17f\\",\\"index\\":0,\\"value\\":13364722,\\"assets\\":[]}", + "{\\"txId\\":\\"1b4ca08ad9b4441abec1509324b184305c0b0c0dda92091a806deacaee3585fa\\",\\"index\\":0,\\"value\\":1198180,\\"assets\\":[{\\"policy_id\\":\\"fca58ef8ba9ef1961e132b611de2f8abcd2f34831e615a6f80c5bb48\\",\\"asset_name\\":\\"77724552472d6c6f656e\\",\\"quantity\\":4966797511}]}", + "{\\"txId\\":\\"756fcffd0cc9517e9b93603de0a802cd124ecf85e7953ecbd2efa2736335294b\\",\\"index\\":0,\\"value\\":1180940,\\"assets\\":[{\\"policy_id\\":\\"fca58ef8ba9ef1961e132b611de2f8abcd2f34831e615a6f80c5bb48\\",\\"asset_name\\":\\"777252534e2d6c6f656e\\",\\"quantity\\":4026628}]}", + "{\\"txId\\":\\"7d729590af3934b4d4894111303bbc215d6dc5bce3ea2feb6ed1e7e7a8a271d9\\",\\"index\\":0,\\"value\\":1180940,\\"assets\\":[{\\"policy_id\\":\\"fca58ef8ba9ef1961e132b611de2f8abcd2f34831e615a6f80c5bb48\\",\\"asset_name\\":\\"546f6b656e2d6c6f656e\\",\\"quantity\\":261493}]}", + "{\\"txId\\":\\"ed299711c6fe5b903eb6f43a5bc7d9c4558c69075d24e9ad2c85e911570d8382\\",\\"index\\":0,\\"value\\":24232705,\\"assets\\":[]}" ] } `; diff --git a/packages/chains/ergo/lib/ErgoChain.ts b/packages/chains/ergo/lib/ErgoChain.ts index eb8b6ca..a20a336 100644 --- a/packages/chains/ergo/lib/ErgoChain.ts +++ b/packages/chains/ergo/lib/ErgoChain.ts @@ -1154,6 +1154,83 @@ class ErgoChain extends AbstractUtxoChain { */ protected serializeTx = (tx: wasm.Transaction): string => Buffer.from(tx.sigma_serialize_bytes()).toString('hex'); + + /** + * verifies consistency within the PaymentTransaction object + * @param transaction the PaymentTransaction + * @returns true if the transaction is verified + */ + verifyPaymentTransaction = async ( + transaction: PaymentTransaction + ): Promise => { + const tx = Serializer.deserialize(transaction.txBytes).unsigned_tx(); + const ergoTx = transaction as ErgoTransaction; + const baseError = `Tx [${transaction.txId}] is not verified: `; + + // verify txId + const txId = tx.id().to_str(); + if (transaction.txId !== txId) { + this.logger.warn( + baseError + + `Transaction ID is inconsistent (expected [${transaction.txId}] found [${txId}])` + ); + return false; + } + + // verify inputBoxes + const txInputs = tx.inputs(); + if (ergoTx.inputBoxes.length !== txInputs.len()) { + this.logger.warn( + baseError + + `ErgoTransaction object input counts is inconsistent [${ + ergoTx.inputBoxes.length + } != ${txInputs.len()}]` + ); + return false; + } + for (let i = 0; i < txInputs.len(); i++) { + const input = txInputs.get(i); + const actualBoxId = input.box_id().to_str(); + const expectedId = wasm.ErgoBox.sigma_parse_bytes(ergoTx.inputBoxes[i]) + .box_id() + .to_str(); + if (expectedId !== actualBoxId) { + this.logger.warn( + baseError + + `BoxId for input at index [${i}] is inconsistent [expected ${expectedId} found ${actualBoxId}]` + ); + return false; + } + } + + // verify dataInputs + const dataInputs = tx.data_inputs(); + if (ergoTx.dataInputs.length !== dataInputs.len()) { + this.logger.warn( + baseError + + `ErgoTransaction object data input counts is inconsistent [${ + ergoTx.dataInputs.length + } != ${dataInputs.len()}]` + ); + return false; + } + for (let i = 0; i < dataInputs.len(); i++) { + const input = dataInputs.get(i); + const actualBoxId = input.box_id().to_str(); + const expectedId = wasm.ErgoBox.sigma_parse_bytes(ergoTx.dataInputs[i]) + .box_id() + .to_str(); + if (expectedId !== actualBoxId) { + this.logger.warn( + baseError + + `BoxId for data input at index [${i}] is inconsistent [expected ${expectedId} found ${actualBoxId}]` + ); + return false; + } + } + + return true; + }; } export default ErgoChain; diff --git a/packages/chains/ergo/tests/ErgoChain.spec.ts b/packages/chains/ergo/tests/ErgoChain.spec.ts index 5f59a4e..f95a177 100644 --- a/packages/chains/ergo/tests/ErgoChain.spec.ts +++ b/packages/chains/ergo/tests/ErgoChain.spec.ts @@ -2277,4 +2277,165 @@ describe('ErgoChain', () => { expect(result.toJson()).toEqual(expectedTx.toJson()); }); }); + + describe('verifyPaymentTransaction', () => { + const network = new TestErgoNetwork(); + + /** + * @target ErgoChain.verifyPaymentTransaction should return true + * when data is consistent + * @dependencies + * @scenario + * - mock a ErgoTransaction + * - run test + * - check returned value + * @expected + * - it should return true + */ + it('should return true when data is consistent', async () => { + // mock a ErgoTransaction + const paymentTx = ErgoTransaction.fromJson( + transactionTestData.transaction5PaymentTransaction + ); + + // run test + const ergoChain = ergoTestUtils.generateChainObject(network); + const result = await ergoChain.verifyPaymentTransaction(paymentTx); + + // check returned value + expect(result).toEqual(true); + }); + + /** + * @target ErgoChain.verifyPaymentTransaction should return false + * when transaction id is wrong + * @dependencies + * @scenario + * - mock a ErgoTransaction with changed txId + * - run test + * - check returned value + * @expected + * - it should return false + */ + it('should return false when transaction id is wrong', async () => { + // mock a ErgoTransaction with changed txId + const paymentTx = ErgoTransaction.fromJson( + transactionTestData.transaction5PaymentTransaction + ); + paymentTx.txId = ergoTestUtils.generateRandomId(); + + // run test + const ergoChain = ergoTestUtils.generateChainObject(network); + const result = await ergoChain.verifyPaymentTransaction(paymentTx); + + // check returned value + expect(result).toEqual(false); + }); + + /** + * @target ErgoChain.verifyPaymentTransaction should return false + * when number of boxes is wrong + * @dependencies + * @scenario + * - mock a ErgoTransaction with less boxes + * - run test + * - check returned value + * @expected + * - it should return false + */ + it('should return false when number of boxes is wrong', async () => { + // mock a ErgoTransaction with less boxes + const paymentTx = ErgoTransaction.fromJson( + transactionTestData.transaction5PaymentTransaction + ); + paymentTx.inputBoxes.pop(); + + // run test + const ergoChain = ergoTestUtils.generateChainObject(network); + const result = await ergoChain.verifyPaymentTransaction(paymentTx); + + // check returned value + expect(result).toEqual(false); + }); + + /** + * @target ErgoChain.verifyPaymentTransaction should return false + * when at least one of the boxes is wrong + * @dependencies + * @scenario + * - mock a ErgoTransaction with changed box + * - run test + * - check returned value + * @expected + * - it should return false + */ + it('should return false when at least one of the boxes is wrong', async () => { + // mock a ErgoTransaction with changed box + const paymentTx = ErgoTransaction.fromJson( + transactionTestData.transaction5PaymentTransaction + ); + const box = ergoTestUtils.toErgoBox(boxTestData.ergoBox1); + paymentTx.inputBoxes[1] = box.sigma_serialize_bytes(); + + // run test + const ergoChain = ergoTestUtils.generateChainObject(network); + const result = await ergoChain.verifyPaymentTransaction(paymentTx); + + // check returned value + expect(result).toEqual(false); + }); + + /** + * @target ErgoChain.verifyPaymentTransaction should return false + * when number of data inputs is wrong + * @dependencies + * @scenario + * - mock a ErgoTransaction with less data inputs + * - run test + * - check returned value + * @expected + * - it should return false + */ + it('should return false when number of data inputs is wrong', async () => { + // mock a ErgoTransaction with less data inputs + const paymentTx = ErgoTransaction.fromJson( + transactionTestData.transaction5PaymentTransaction + ); + paymentTx.dataInputs.pop(); + + // run test + const ergoChain = ergoTestUtils.generateChainObject(network); + const result = await ergoChain.verifyPaymentTransaction(paymentTx); + + // check returned value + expect(result).toEqual(false); + }); + + /** + * @target ErgoChain.verifyPaymentTransaction should return false + * when at least one of the data inputs is wrong + * @dependencies + * @scenario + * - mock a ErgoTransaction with changed data input + * - run test + * - check returned value + * @expected + * - it should return false + */ + it('should return false when at least one of the data inputs is wrong', async () => { + // mock a ErgoTransaction with changed data input + const paymentTx = ErgoTransaction.fromJson( + transactionTestData.transaction5PaymentTransaction + ); + const box = ergoTestUtils.toErgoBox(boxTestData.ergoBox1); + paymentTx.dataInputs[0] = box.sigma_serialize_bytes(); + + // run test + const ergoChain = ergoTestUtils.generateChainObject(network); + const result = await ergoChain.verifyPaymentTransaction(paymentTx); + + // check returned value + expect(result).toEqual(false); + }); + }); }); diff --git a/packages/chains/evm/lib/EvmChain.ts b/packages/chains/evm/lib/EvmChain.ts index 5d9555c..53ea8fb 100644 --- a/packages/chains/evm/lib/EvmChain.ts +++ b/packages/chains/evm/lib/EvmChain.ts @@ -860,6 +860,29 @@ abstract class EvmChain extends AbstractChain { ); return wrappedAssets; }; + + /** + * verifies consistency within the PaymentTransaction object + * @param transaction the PaymentTransaction + * @returns true if the transaction is verified + */ + verifyPaymentTransaction = async ( + transaction: PaymentTransaction + ): Promise => { + const tx = Serializer.deserialize(transaction.txBytes); + const baseError = `Tx [${transaction.txId}] is not verified: `; + + // verify txId + if (transaction.txId !== tx.unsignedHash) { + this.logger.warn( + baseError + + `Transaction id is inconsistent (expected [${transaction.txId}] found [${tx.unsignedHash}])` + ); + return false; + } + + return true; + }; } export default EvmChain; diff --git a/packages/chains/evm/tests/EvmChain.spec.ts b/packages/chains/evm/tests/EvmChain.spec.ts index b2abaac..8eedafe 100644 --- a/packages/chains/evm/tests/EvmChain.spec.ts +++ b/packages/chains/evm/tests/EvmChain.spec.ts @@ -2512,4 +2512,68 @@ describe('EvmChain', () => { expect(result).toEqual(false); }); }); + + describe('verifyPaymentTransaction', () => { + const network = new TestEvmNetwork(); + + /** + * @target EvmChain.verifyPaymentTransaction should return true + * when data is consistent + * @dependencies + * @scenario + * - mock a PaymentTransaction + * - run test + * - check returned value + * @expected + * - it should return true + */ + it('should return true when data is consistent', async () => { + // mock a PaymentTransaction + const evmChain = testUtils.generateChainObject(network); + const tx = Transaction.from(TestData.transaction1Json); + const paymentTx = new PaymentTransaction( + evmChain.CHAIN, + tx.unsignedHash, + '', + Serializer.serialize(tx), + TransactionType.manual + ); + + // run test + const result = await evmChain.verifyPaymentTransaction(paymentTx); + + // check returned value + expect(result).toEqual(true); + }); + + /** + * @target EvmChain.verifyPaymentTransaction should return false + * when transaction id is wrong + * @dependencies + * @scenario + * - mock a PaymentTransaction with changed txId + * - run test + * - check returned value + * @expected + * - it should return false + */ + it('should return false when transaction id is wrong', async () => { + // mock a PaymentTransaction with changed txId + const evmChain = testUtils.generateChainObject(network); + const tx = Transaction.from(TestData.transaction1Json); + const paymentTx = new PaymentTransaction( + evmChain.CHAIN, + '904888037623f5a73eab986f2caf0f8765cd86ea1deeaedbb83de14f67e40874', + '', + Serializer.serialize(tx), + TransactionType.manual + ); + + // run test + const result = await evmChain.verifyPaymentTransaction(paymentTx); + + // check returned value + expect(result).toEqual(false); + }); + }); });