Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/wallet/wallet-toolbox/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@bsv/wallet-toolbox",
"version": "2.3.0",
"version": "2.3.1",
"description": "BRC100 conforming wallet, wallet storage and wallet signer components",
"main": "./out/src/index.js",
"types": "./out/src/index.d.ts",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ import { WERR_INTERNAL, WERR_INVALID_PARAMETER } from '../../sdk/WERR_errors'
* and merge rules are added to the arguments passed to the storage layer.
*
* The existing transaction's `status` determines what the merge path does next:
* - `'unproven'` or `'completed'`: outputs are merged; status is left as-is.
* - `'unproven'`, `'completed'`, or `'sending'`: outputs are merged; status is left as-is.
* The `'sending'` case covers a transaction this wallet already signed and handed to broadcast processing, but
* whose proven_tx_req has not yet been advanced by the normal monitor/posting flow.
* - `'nosend'`: the `internalizeAction` call is treated as explicit authorization to advance the lifecycle.
* `transactions.status` is promoted to `'completed'` (BUMP-bearing BEEF) or `'unproven'` (otherwise), and the
* `proven_tx_req` is moved out of `'nosend'` so Monitor's proof-fetching flow can finalize it. See storage-layer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,83 @@ describe('WalletStorageManager tests', () => {
expect(result).toBeTruthy()
}
})

test('3_internalize same-wallet payment while transaction is sending', async () => {
for (const { wallet, activeStorage, identityKey, userId } of ctxs) {
const outputSatoshis = 5
const derivationPrefix = Buffer.from('same-wallet-invoice').toString('base64')
const derivationSuffix = Buffer.from('utxo-0').toString('base64')
const brc29ProtocolID: bsv.WalletProtocol = [2, '3241645161d8']
const derivedPublicKey = wallet.keyDeriver.derivePublicKey(
brc29ProtocolID,
`${derivationPrefix} ${derivationSuffix}`,
identityKey
)

const cr = await wallet.createAction({
description: 'same-wallet payment pending send',
outputs: [
{
satoshis: outputSatoshis,
lockingScript: new bsv.P2PKH().lock(derivedPublicKey.toAddress()).toHex(),
outputDescription: 'pay same wallet'
}
],
options: {
returnTXIDOnly: false,
randomizeOutputs: false,
signAndProcess: true,
noSend: true
}
})
expect(cr.tx).toBeTruthy()
expect(cr.txid).toBeTruthy()
if (cr.tx == null || cr.txid == null) throw new Error('createAction did not return a signed transaction')

const existingTx = (await activeStorage.findTransactions({
partial: { userId, txid: cr.txid }
}))[0]
expect(existingTx).toBeTruthy()
await activeStorage.updateTransaction(existingTx.transactionId, { status: 'sending' })

// Mock chaintracker responses for AtomicBEEF validation to avoid flakiness from
// live chaintracks endpoints. This test exercises the 'sending' merge path for a
// same-wallet internalize before monitor completion; it is not testing proof validity
// (createAction + other tests cover BEEF construction/validation).
activeStorage.setServices({
getChainTracker: async () => ({
isValidRootForHeight: async (_root: string, _height: number) => true
})
} as any)

const ir = await activeStorage.internalizeAction(
{ userId, identityKey },
{
tx: cr.tx,
outputs: [
{
outputIndex: 0,
protocol: 'wallet payment',
paymentRemittance: {
derivationPrefix,
derivationSuffix,
senderIdentityKey: identityKey
}
}
],
description: 'received same-wallet payment before monitor completion'
}
)

expect(ir.accepted).toBe(true)
expect(ir.isMerge).toBe(true)

const updatedTx = (await activeStorage.findTransactions({
partial: { transactionId: existingTx.transactionId }
}))[0]
expect(updatedTx.status).toBe('sending')
}
})
})
function logger (s: string) {
process.stdout.write(`${s}\n`)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,9 @@ export async function restoreInputsToSpendable (
* and merge rules are added to the arguments passed to the storage layer.
*
* The existing transaction's `status` determines what the merge path does next:
* - `'unproven'` or `'completed'`: outputs are merged into the existing record. The transaction status is left as-is.
* - `'unproven'`, `'completed'`, or `'sending'`: outputs are merged into the existing record. The transaction status is left as-is.
* The `'sending'` case covers a transaction this wallet already signed and handed to broadcast processing, but
* whose proven_tx_req has not yet been advanced by the normal monitor/posting flow.
* - `'nosend'`: an ambiguous case. The transaction was created with `noSend: true` and may have been externally
* broadcast, may be sitting in a sendWith chain, or may be stuck mid-flight. The merge path treats the
* `internalizeAction` call as explicit authorization to advance the lifecycle. Specifically: `transactions.status`
Expand Down Expand Up @@ -322,7 +324,7 @@ class InternalizeActionContext {
partial: { userId: this.userId, txid: this.txid }
})
)
if ((this.etx != null) && this.etx.status !== 'completed' && this.etx.status !== 'unproven' && this.etx.status !== 'nosend') {
if ((this.etx != null) && this.etx.status !== 'completed' && this.etx.status !== 'unproven' && this.etx.status !== 'sending' && this.etx.status !== 'nosend') {
throw new WERR_INVALID_PARAMETER(
'tx',
`target transaction of internalizeAction has invalid status ${this.etx.status}.`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ describe.skip('internalizeAction tests', () => {
}
})

// Check: 'unproven' or 'completed' status. Any other status is an error.
// Check: 'unproven', 'completed', or 'sending' status for merge (status left as-is). 'nosend' advances lifecycle.
// Any other status is an error.
// When the transaction already exists, the description is updated. The isOutgoing sense is not changed.

test.skip('1_default real wallet data', async () => {
Expand Down