Skip to content

Commit 433562e

Browse files
authored
Merge pull request #4 from bsv-blockchain/feat/direct-payment
feat: add BRC-29 direct payment internalization to WalletCore
2 parents 1da0c45 + 856e533 commit 433562e

File tree

8 files changed

+1138
-51
lines changed

8 files changed

+1138
-51
lines changed

CLAUDE.md

Lines changed: 59 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,15 @@ const wallet = await createWallet({ network: 'main' })
110110
|--------|--------|---------|-------------|
111111
| `pay(options)` | `PaymentOptions` | `Promise<TransactionResult>` | Payment via MessageBox P2P (PeerPayClient) |
112112
| `send(options)` | `SendOptions` | `Promise<SendResult>` | Multi-output: P2PKH + OP_RETURN + PushDrop in one tx |
113-
| `fundServerWallet(request, basket?)` | `PaymentRequest, string?` | `Promise<TransactionResult>` | Fund a ServerWallet using BRC-29 derivation |
113+
| `fundServerWallet(request, basket?)` | `PaymentRequest, string?` | `Promise<TransactionResult>` | Fund a ServerWallet using BRC-29 derivation (legacy) |
114+
115+
### Direct Payments — BRC-29 Wallet Payment Internalization (WalletCore)
116+
117+
| Method | Params | Returns | Description |
118+
|--------|--------|---------|-------------|
119+
| `createPaymentRequest(options)` | `{ satoshis, memo? }` | `PaymentRequest` | Generate BRC-29 derivation data for someone to pay you |
120+
| `sendDirectPayment(request)` | `PaymentRequest` | `Promise<DirectPaymentResult>` | Create BRC-29 derived P2PKH tx + return remittance data |
121+
| `receiveDirectPayment(payment)` | `IncomingPayment` | `Promise<void>` | Internalize into wallet balance via `wallet payment` (NOT into a basket) |
114122

115123
### Tokens (tokens module)
116124

@@ -193,12 +201,11 @@ const wallet = await ServerWallet.create({
193201

194202
| Method | Params | Returns | Description |
195203
|--------|--------|---------|-------------|
196-
| `createPaymentRequest(options)` | `{ satoshis, memo? }` | `PaymentRequest` | Generate BRC-29 payment request for desktop client |
197-
| `receivePayment(payment)` | `IncomingPayment` | `Promise<void>` | Internalize payment from desktop using `wallet payment` protocol |
204+
| `receivePayment(payment)` | `IncomingPayment` | `Promise<void>` | **Deprecated.** Use `receiveDirectPayment()` (inherited from WalletCore). Kept for backward compat with `server_funding` label. |
198205

199206
### Shared Methods
200207

201-
ServerWallet has all the same methods as BrowserWallet (pay, send, createToken, inscribeText, etc.) via the same module composition pattern.
208+
ServerWallet has all the same methods as BrowserWallet (pay, send, createToken, inscribeText, createPaymentRequest, sendDirectPayment, receiveDirectPayment, etc.) via the same module composition pattern.
202209

203210
---
204211

@@ -714,32 +721,64 @@ const incoming = await wallet.listIncomingPayments()
714721
await wallet.acceptIncomingPayment(incoming[0], 'received-payments')
715722
```
716723

717-
### 7.8 Server Wallet: Create, Fund, Receive, Balance
724+
### 7.8 Direct Payments (BRC-29 Wallet Payment Internalization)
718725

719726
```typescript
720-
// Server side: create wallet
721-
const { ServerWallet } = await import('@bsv/simple/server')
722-
const server = await ServerWallet.create({ privateKey: 'hex', network: 'main' })
727+
// Direct payments work on BOTH browser and server wallets.
728+
// Funds go directly into the wallet's spendable balance (NOT into a basket).
729+
730+
// --- Flow: Browser pays Server ---
723731

724732
// Server: generate payment request
725-
const request = server.createPaymentRequest({ satoshis: 50000 })
733+
const request = serverWallet.createPaymentRequest({ satoshis: 2000 })
726734

727-
// Client: fund server wallet
728-
const result = await wallet.fundServerWallet(request, 'server-funding')
735+
// Browser: create BRC-29 derived P2PKH transaction
736+
const payment = await browserWallet.sendDirectPayment(request)
729737

730-
// Client: send tx to server
731-
await fetch('/api/server-wallet?action=receive', {
738+
// Browser: send tx + remittance to server (via API)
739+
await fetch('/api/receive-payment', {
732740
method: 'POST',
741+
headers: { 'Content-Type': 'application/json' },
733742
body: JSON.stringify({
734-
tx: Array.from(result.tx),
735-
senderIdentityKey: wallet.getIdentityKey(),
736-
derivationPrefix: request.derivationPrefix,
737-
derivationSuffix: request.derivationSuffix,
738-
outputIndex: 0
743+
tx: Array.from(payment.tx),
744+
senderIdentityKey: payment.senderIdentityKey,
745+
derivationPrefix: payment.derivationPrefix,
746+
derivationSuffix: payment.derivationSuffix,
747+
outputIndex: payment.outputIndex
739748
})
740749
})
741750

742-
// Server: receive payment
751+
// Server: internalize
752+
await serverWallet.receiveDirectPayment({ tx, senderIdentityKey, derivationPrefix, derivationSuffix, outputIndex: 0 })
753+
754+
// --- Flow: Server pays Browser ---
755+
756+
// Browser: create payment request
757+
const request = browserWallet.createPaymentRequest({ satoshis: 100 })
758+
// ... send request to server via API ...
759+
760+
// Server: create payment
761+
const payment = await serverWallet.sendDirectPayment(request)
762+
// ... return payment data to browser ...
763+
764+
// Browser: internalize into wallet balance
765+
await browserWallet.receiveDirectPayment({
766+
tx: paymentData.tx,
767+
senderIdentityKey: paymentData.senderIdentityKey,
768+
derivationPrefix: paymentData.derivationPrefix,
769+
derivationSuffix: paymentData.derivationSuffix,
770+
outputIndex: paymentData.outputIndex
771+
})
772+
```
773+
774+
### 7.8b Server Wallet: Legacy Fund Flow
775+
776+
```typescript
777+
// Legacy pattern — prefer sendDirectPayment/receiveDirectPayment instead
778+
const { ServerWallet } = await import('@bsv/simple/server')
779+
const server = await ServerWallet.create({ privateKey: 'hex', network: 'main' })
780+
const request = server.createPaymentRequest({ satoshis: 50000 })
781+
const result = await wallet.fundServerWallet(request, 'server-funding')
743782
await server.receivePayment({ tx, senderIdentityKey, derivationPrefix, derivationSuffix, outputIndex: 0 })
744783
```
745784

@@ -873,6 +912,7 @@ interface InscriptionResult extends TransactionResult { type: InscriptionType; d
873912
interface ServerWalletConfig { privateKey: string; network?: Network; storageUrl?: string }
874913
interface PaymentRequest { serverIdentityKey: string; derivationPrefix: string; derivationSuffix: string; satoshis: number; memo?: string }
875914
interface IncomingPayment { tx: number[] | Uint8Array; senderIdentityKey: string; derivationPrefix: string; derivationSuffix: string; outputIndex: number; description?: string }
915+
interface DirectPaymentResult extends TransactionResult { senderIdentityKey: string; derivationPrefix: string; derivationSuffix: string; outputIndex: number }
876916
```
877917

878918
### DID Types

jest.config.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/** @type {import('jest').Config} */
2+
module.exports = {
3+
preset: 'ts-jest',
4+
testEnvironment: 'node',
5+
roots: ['<rootDir>/src'],
6+
testMatch: ['**/__tests__/**/*.test.ts'],
7+
moduleFileExtensions: ['ts', 'js', 'json'],
8+
transformIgnorePatterns: [
9+
'node_modules/(?!(@bsv)/)'
10+
]
11+
}

src/core/WalletCore.ts

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ import {
2020
SendResult,
2121
SendOutputDetail,
2222
TransactionResult,
23-
PaymentRequest
23+
PaymentRequest,
24+
IncomingPayment,
25+
DirectPaymentResult
2426
} from './types'
2527

2628
export abstract class WalletCore {
@@ -224,6 +226,120 @@ export abstract class WalletCore {
224226
}
225227
}
226228

229+
// ============================================================================
230+
// Direct Payment (BRC-29 wallet payment internalization)
231+
// ============================================================================
232+
233+
/**
234+
* Generate a payment request containing BRC-29 derivation data.
235+
* Share this with the sender so they can create a payment via `sendDirectPayment()`.
236+
*/
237+
createPaymentRequest (options: { satoshis: number, memo?: string }): PaymentRequest {
238+
const derivationPrefix = Utils.toBase64(Utils.toArray('payment', 'utf8'))
239+
const derivationSuffix = Utils.toBase64(Random(8))
240+
return {
241+
serverIdentityKey: this.identityKey,
242+
derivationPrefix,
243+
derivationSuffix,
244+
satoshis: options.satoshis,
245+
memo: options.memo
246+
}
247+
}
248+
249+
/**
250+
* Create a BRC-29 derived P2PKH transaction for the recipient described in the request.
251+
* Returns the transaction plus remittance data the recipient needs to call `receiveDirectPayment()`.
252+
*/
253+
async sendDirectPayment (request: PaymentRequest): Promise<DirectPaymentResult> {
254+
try {
255+
const client = this.getClient()
256+
const protocolID: [SecurityLevel, string] = [2 as SecurityLevel, '3241645161d8']
257+
const keyID = `${request.derivationPrefix} ${request.derivationSuffix}`
258+
259+
const { publicKey: derivedKey } = await client.getPublicKey({
260+
protocolID,
261+
keyID,
262+
counterparty: request.serverIdentityKey,
263+
forSelf: false
264+
})
265+
266+
const lockingScript = new P2PKH()
267+
.lock(PublicKey.fromString(derivedKey).toAddress())
268+
.toHex()
269+
270+
const outputs: any[] = [{
271+
lockingScript,
272+
satoshis: request.satoshis,
273+
outputDescription: `Direct payment: ${request.satoshis} sats`,
274+
customInstructions: JSON.stringify({
275+
derivationPrefix: request.derivationPrefix,
276+
derivationSuffix: request.derivationSuffix,
277+
payee: request.serverIdentityKey
278+
})
279+
}]
280+
281+
if (request.memo != null && request.memo !== '') {
282+
const memoScript = new Script()
283+
.writeOpCode(OP.OP_FALSE)
284+
.writeOpCode(OP.OP_RETURN)
285+
.writeBin(Array.from(Utils.toArray(request.memo, 'utf8')))
286+
outputs.push({
287+
lockingScript: memoScript.toHex(),
288+
satoshis: 0,
289+
outputDescription: 'Payment memo'
290+
})
291+
}
292+
293+
const result = await client.createAction({
294+
description: request.memo ?? `Direct payment (${request.satoshis} sats)`,
295+
outputs,
296+
options: { randomizeOutputs: false, acceptDelayedBroadcast: false }
297+
})
298+
299+
return {
300+
txid: result.txid ?? '',
301+
tx: result.tx,
302+
senderIdentityKey: this.identityKey,
303+
derivationPrefix: request.derivationPrefix,
304+
derivationSuffix: request.derivationSuffix,
305+
outputIndex: 0
306+
}
307+
} catch (error) {
308+
throw new Error(`Direct payment failed: ${(error as Error).message}`)
309+
}
310+
}
311+
312+
/**
313+
* Internalize a received payment directly into the wallet's spendable balance
314+
* using the `wallet payment` protocol. This does NOT put the output into a basket —
315+
* it becomes a regular spendable UTXO managed by the wallet.
316+
*/
317+
async receiveDirectPayment (payment: IncomingPayment): Promise<void> {
318+
try {
319+
const client = this.getClient()
320+
const tx = payment.tx instanceof Uint8Array
321+
? Array.from(payment.tx)
322+
: payment.tx
323+
324+
await (client as any).internalizeAction({
325+
tx,
326+
outputs: [{
327+
outputIndex: payment.outputIndex,
328+
protocol: 'wallet payment',
329+
paymentRemittance: {
330+
senderIdentityKey: payment.senderIdentityKey,
331+
derivationPrefix: payment.derivationPrefix,
332+
derivationSuffix: payment.derivationSuffix
333+
}
334+
}],
335+
description: payment.description ?? `Payment from ${payment.senderIdentityKey.substring(0, 20)}...`,
336+
labels: ['direct_payment']
337+
})
338+
} catch (error) {
339+
throw new Error(`Failed to receive direct payment: ${(error as Error).message}`)
340+
}
341+
}
342+
227343
// ============================================================================
228344
// Fund Server Wallet
229345
// ============================================================================

0 commit comments

Comments
 (0)