-
Notifications
You must be signed in to change notification settings - Fork 15
feat: add UTXO release mechanism for nano contract transactions #1050
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
97b872a
32c8a5b
bec35fd
c729dc0
2e4b998
de8976f
c394ff5
3b76e9b
117a896
02733be
f783bb1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,155 @@ | ||
| /** | ||
| * Copyright (c) Hathor Labs and its affiliates. | ||
| * | ||
| * This source code is licensed under the MIT license found in the | ||
| * LICENSE file in the root directory of this source tree. | ||
| */ | ||
|
|
||
| import { isEmpty } from 'lodash'; | ||
| import { GenesisWalletHelper } from '../helpers/genesis-wallet.helper'; | ||
| import { | ||
| generateWalletHelper, | ||
| stopAllWallets, | ||
| waitForTxReceived, | ||
| waitTxConfirmed, | ||
| } from '../helpers/wallet.helper'; | ||
| import HathorWallet from '../../../src/new/wallet'; | ||
| import { NATIVE_TOKEN_UID, NANO_CONTRACTS_INITIALIZE_METHOD } from '../../../src/constants'; | ||
| import { NanoContractTransactionError } from '../../../src/errors'; | ||
| import { SendTransaction } from '../../../src'; | ||
|
|
||
| describe('Nano contract UTXO lock/unlock lifecycle', () => { | ||
|
tuliomir marked this conversation as resolved.
|
||
| let hWallet: HathorWallet; | ||
| let contractId: string; | ||
|
|
||
| const checkTxValid = async (wallet: HathorWallet, tx: { hash?: string | null }) => { | ||
| const txId = tx.hash!; | ||
| expect(txId).toBeDefined(); | ||
| await waitForTxReceived(wallet, txId); | ||
| await waitTxConfirmed(wallet, txId); | ||
| const txAfterExecution = await wallet.getFullTxById(txId); | ||
| expect(isEmpty(txAfterExecution.meta.voided_by)).toBe(true); | ||
| expect(txAfterExecution.meta.first_block).not.toBeNull(); | ||
| }; | ||
|
|
||
| beforeAll(async () => { | ||
| hWallet = await generateWalletHelper(); | ||
| const address = await hWallet.getAddressAtIndex(0); | ||
|
|
||
| await GenesisWalletHelper.injectFunds(hWallet, address, 10000n, {}); | ||
|
|
||
| const initTx = await hWallet.createAndSendNanoContractTransaction( | ||
| NANO_CONTRACTS_INITIALIZE_METHOD, | ||
| address, | ||
| { | ||
| blueprintId: global.FEE_BLUEPRINT_ID, | ||
| args: [], | ||
| actions: [ | ||
| { | ||
| type: 'deposit', | ||
| token: NATIVE_TOKEN_UID, | ||
| amount: 1000n, | ||
| changeAddress: address, | ||
| }, | ||
| ], | ||
| } | ||
| ); | ||
| await checkTxValid(hWallet, initTx); | ||
| contractId = initTx.hash!; | ||
| }); | ||
|
|
||
| afterAll(async () => { | ||
| await hWallet.stop(); | ||
| await stopAllWallets(); | ||
| await GenesisWalletHelper.clearListeners(); | ||
| }); | ||
|
|
||
| it('should lock UTXOs on prepare and unlock with releaseUtxos()', async () => { | ||
| const address = await hWallet.getAddressAtIndex(0); | ||
|
|
||
| // Use full balance to ensure all UTXOs are consumed | ||
| const balanceMap = await hWallet.getBalance(NATIVE_TOKEN_UID); | ||
| const availableBalance = balanceMap[0]?.balance?.unlocked ?? 0n; | ||
| expect(availableBalance).toBeGreaterThan(0n); | ||
| const depositAmount = availableBalance; | ||
|
|
||
| let activeSendTx: SendTransaction | null = null; | ||
| try { | ||
| // Step 1: Prepare tx (UTXOs get locked) | ||
| const sendTx1: SendTransaction = await hWallet.createNanoContractTransaction( | ||
| 'noop', | ||
| address, | ||
| { | ||
| ncId: contractId, | ||
| args: [], | ||
| actions: [ | ||
| { | ||
| type: 'deposit', | ||
| token: NATIVE_TOKEN_UID, | ||
| amount: depositAmount, | ||
| changeAddress: address, | ||
| }, | ||
| ], | ||
| }, | ||
| { signTx: false } | ||
| ); | ||
| activeSendTx = sendTx1; | ||
|
|
||
| expect(sendTx1.transaction).not.toBeNull(); | ||
| expect(sendTx1.transaction!.inputs.length).toBeGreaterThan(0); | ||
|
|
||
| // Step 2: Second prepare should fail (UTXOs still locked) | ||
| await expect( | ||
| hWallet.createNanoContractTransaction( | ||
| 'noop', | ||
| address, | ||
| { | ||
| ncId: contractId, | ||
| args: [], | ||
| actions: [ | ||
| { | ||
| type: 'deposit', | ||
| token: NATIVE_TOKEN_UID, | ||
| amount: depositAmount, | ||
| changeAddress: address, | ||
| }, | ||
| ], | ||
| }, | ||
| { signTx: false } | ||
| ) | ||
| ).rejects.toThrow(NanoContractTransactionError); | ||
|
|
||
| // Step 3: Release locked UTXOs | ||
| await sendTx1.releaseUtxos(); | ||
| activeSendTx = null; | ||
|
|
||
| // Step 4: Prepare again (should succeed after unlock) | ||
| const sendTx3: SendTransaction = await hWallet.createNanoContractTransaction( | ||
| 'noop', | ||
| address, | ||
| { | ||
| ncId: contractId, | ||
| args: [], | ||
| actions: [ | ||
| { | ||
| type: 'deposit', | ||
| token: NATIVE_TOKEN_UID, | ||
| amount: depositAmount, | ||
| changeAddress: address, | ||
| }, | ||
| ], | ||
| }, | ||
| { signTx: false } | ||
| ); | ||
| activeSendTx = sendTx3; | ||
|
|
||
| expect(sendTx3.transaction).not.toBeNull(); | ||
| expect(sendTx3.transaction!.inputs.length).toBeGreaterThan(0); | ||
|
|
||
| await sendTx3.releaseUtxos(); | ||
| activeSendTx = null; | ||
| } finally { | ||
| await activeSendTx?.releaseUtxos(); | ||
| } | ||
| }); | ||
| }); | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -938,6 +938,18 @@ class SendTransactionWalletService extends EventEmitter implements ISendTransact | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Release all UTXOs that were marked as selected for this transaction. | ||
| * No-op: wallet-service manages UTXO state server-side. | ||
| * | ||
| * @memberof SendTransactionWalletService | ||
| * @inner | ||
| */ | ||
| // eslint-disable-next-line class-methods-use-this | ||
| async releaseUtxos(): Promise<void> { | ||
| // No-op: wallet-service manages UTXO state server-side | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question: Is there a plan to implement this feature for the Wallet Service too? An API call to release the stuck UTXOs?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably not, because the way wallet service locks the utxos is by binding them to a tx proposal. In this use case that we are dealing, we never create a tx proposal before the user approves |
||
| } | ||
|
|
||
| /** | ||
| * Run sendTransaction from preparing, i.e. prepare, sign, mine and send the tx | ||
| * | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.