Skip to content
Draft
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
47 changes: 46 additions & 1 deletion packages/adapter-tests/src/integration.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Repositories, Manager, Logger } from 'coco-cashu-core';
import { initializeCoco, getEncodedToken } from 'coco-cashu-core';
import { initializeCoco, getEncodedToken, ConsoleLogger } from 'coco-cashu-core';
import {
Mint,
Wallet,
Expand All @@ -10,9 +10,12 @@ import {
type MintKeys,
type Token,
type HasKeysetKeys,
parseP2PKSecret,
type Secret
} from '@cashu/cashu-ts';
import { createFakeInvoice } from 'fake-bolt11';


export type OutputDataFactory = (amount: number, keys: MintKeys | HasKeysetKeys) => OutputData;

export type IntegrationTestRunner = {
Expand Down Expand Up @@ -2035,6 +2038,48 @@ export async function runIntegrationTests<TRepositories extends Repositories = R
await expect(mgr!.wallet.receive(p2pkToken)).rejects.toThrow();
});

it('should send P2PK locked tokens using prepareSendP2pk', async () => {
// Generate a keypair for the recipient
const recipientKeypair = await mgr!.keyring.generateKeyPair();
expect(recipientKeypair.publicKeyHex).toBeDefined();

const balanceBefore = await mgr!.wallet.getBalances();
const amountBefore = balanceBefore[mintUrl] || 0;

// Send P2PK locked tokens using the new API
const sendAmount = 30;
const preparedSend = await mgr!.send.prepareSendP2pk(
mintUrl,
sendAmount,
recipientKeypair.publicKeyHex,
);
expect(preparedSend.state).toBe('prepared');
expect(preparedSend.method).toBe('p2pk');

const { token } = await mgr!.send.executePreparedSend(preparedSend.id);

// Verify the token proofs are P2PK locked
expect(token.proofs.length).toBeGreaterThan(0);
const firstProof = token.proofs[0];
expect(firstProof?.secret).toBeDefined();
const parsedSecret: Secret = parseP2PKSecret(firstProof!.secret);
expect(parsedSecret[0]).toBe('P2PK');
expect(parsedSecret[1].data).toBe(recipientKeypair.publicKeyHex);

// Balance should have decreased
const balanceAfter = await mgr!.wallet.getBalances();
const amountAfter = balanceAfter[mintUrl] || 0;
expect(amountAfter).toBeLessThan(amountBefore);

// Receive the P2PK token (we have the private key)
await mgr!.wallet.receive(token);

// Balance should be restored (minus fees)
const balanceFinal = await mgr!.wallet.getBalances();
const amountFinal = balanceFinal[mintUrl] || 0;
expect(amountFinal).toBeGreaterThan(amountAfter);
});

it('should handle multiple P2PK locked proofs in one token', async () => {
// Generate a keypair using the KeyRing API
const keypair = await mgr!.keyring.generateKeyPair();
Expand Down
8 changes: 8 additions & 0 deletions packages/core/Manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ import {
MintRequestProvider,
MeltBolt11Handler,
MeltHandlerProvider,
SendHandlerProvider,
DefaultSendHandler,
P2pkSendHandler,
} from './infra';
import { EventBus, type CoreEvents } from './events';
import { type Logger, NullLogger } from './logging';
Expand Down Expand Up @@ -637,13 +640,18 @@ export class Manager {
);

const sendOperationLogger = this.getChildLogger('SendOperationService');
const sendHandlerProvider = new SendHandlerProvider({
default: new DefaultSendHandler(),
p2pk: new P2pkSendHandler(),
});
const sendOperationService = new SendOperationService(
repositories.sendOperationRepository,
repositories.proofRepository,
proofService,
mintService,
walletService,
this.eventBus,
sendHandlerProvider,
sendOperationLogger,
);
const sendOperationRepository = repositories.sendOperationRepository;
Expand Down
28 changes: 28 additions & 0 deletions packages/core/api/SendApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import type {
SendOperation,
PreparedSendOperation,
PendingSendOperation,
CreateSendOperationOptions,
} from '../operations/send/SendOperation';
import type { SendMethod, SendMethodData } from '../operations/send/SendMethodHandler';

/**
* API for managing send operations.
Expand Down Expand Up @@ -42,6 +44,32 @@ export class SendApi {
return this.sendOperationService.prepare(initOp);
}

/**
* Prepare a P2PK (Pay-to-Public-Key) send operation.
* Creates tokens that are locked to a specific public key and can only be
* redeemed by the holder of the corresponding private key.
*
* Use this when you want to send tokens to a specific recipient identified
* by their public key. The recipient must sign the proofs with their private
* key to redeem them.
*
* @param mintUrl - The mint URL to send from
* @param amount - The amount to send
* @param pubkey - The recipient's public key (hex-encoded, 33 bytes compressed)
* @returns The prepared operation with fee information
*/
async prepareSendP2pk(
mintUrl: string,
amount: number,
pubkey: string,
): Promise<PreparedSendOperation> {
const initOp = await this.sendOperationService.init(mintUrl, amount, {
method: 'p2pk',
methodData: { pubkey },
});
return this.sendOperationService.prepare(initOp);
}

/**
* Execute a prepared send operation.
* Call this after `prepareSend()` to complete the send.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type {
MeltMethod,
MeltMethodHandler,
MeltMethodHandlerRegistry,
} from '../../operations/melt/MeltMethodHandler';
} from '../../../operations/melt/MeltMethodHandler';

/**
* Runtime registry for melt method handlers.
Expand Down
Loading