Skip to content

Commit

Permalink
feat: tweaked invoice rpcs (#73)
Browse files Browse the repository at this point in the history
* feat: tweaked invoice rpcs

* feat: tests and keyPair util

* chore: pnpm lock

* fix: use browser window crpyto

* chore: createInvoiceTweaked test

* chore: revert pnpm-lock changes

* chore: move crypto to test

* chore: remove wildcard import

* fix: rpc tests

* chore: bump wasm bundles

---------

Co-authored-by: Alex Lewin <[email protected]>
  • Loading branch information
Kodylow and alexlwn123 authored Oct 12, 2024
1 parent 4d8b515 commit d0a7432
Show file tree
Hide file tree
Showing 15 changed files with 2,600 additions and 2,883 deletions.
6 changes: 6 additions & 0 deletions .changeset/afraid-planets-float.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@fedimint/fedimint-client-wasm-bundler': patch
'@fedimint/fedimint-client-wasm-web': patch
---

Adds tweak invoice rpcs
2 changes: 2 additions & 0 deletions packages/core-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^11.1.6",
"@types/node": "^20.16.10",
"@types/secp256k1": "^4.0.6",
"rollup": "^4.24.0",
"rollup-plugin-dts": "^6.1.1",
"rollup-plugin-typescript2": "^0.36.0",
"secp256k1": "^5.0.0",
"tslib": "^2.7.0",
"typescript": "^5.6.2"
}
Expand Down
108 changes: 71 additions & 37 deletions packages/core-web/src/services/LightningService.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { expect } from 'vitest'
import { walletTest } from '../test/setupTests'
import { keyPair } from '../test/crypto'

walletTest(
'createInvoice should create a bolt11 invoice',
Expand Down Expand Up @@ -86,42 +87,6 @@ walletTest('getGateway should return a gateway', async ({ wallet }) => {
})
})

walletTest(
'createInvoiceWithGateway should create a bolt11 invoice with a gateway',
async ({ wallet }) => {
expect(wallet).toBeDefined()
expect(wallet.isOpen()).toBe(true)

const gateways = await wallet.lightning.listGateways()
const gateway = gateways[0]
expect(gateway).toBeDefined()

const counterBefore = wallet.testing.getRequestCounter()
const invoice = await wallet.lightning.createInvoiceWithGateway(
100,
'test',
null,
{},
gateway.info,
)
expect(invoice).toBeDefined()
expect(invoice).toMatchObject({
invoice: expect.any(String),
operation_id: expect.any(String),
})
expect(wallet.testing.getRequestCounter()).toBe(counterBefore + 1)
await expect(
wallet.lightning.createInvoiceWithGateway(
100,
'test',
1000,
{},
gateway.info,
),
).resolves.toBeDefined()
},
)

walletTest(
'payInvoice should throw on insufficient funds',
async ({ wallet }) => {
Expand All @@ -138,7 +103,7 @@ walletTest(
const counterBefore = wallet.testing.getRequestCounter()
// Insufficient funds
try {
await wallet.lightning.payInvoice(invoice.invoice, {})
await wallet.lightning.payInvoice(invoice.invoice)
expect.unreachable('Should throw error')
} catch (error) {
expect(error).toBeDefined()
Expand Down Expand Up @@ -178,3 +143,72 @@ walletTest(
})
},
)

walletTest(
'createInvoiceTweaked should create a bolt11 invoice with a tweaked public key',
async ({ wallet }) => {
expect(wallet).toBeDefined()
expect(wallet.isOpen()).toBe(true)

// Make an ephemeral key pair
const { publicKey, secretKey } = keyPair()
const tweak = 1

// Create an invoice paying to the tweaked public key
const invoice = await wallet.lightning.createInvoiceTweaked(
1000,
'test tweaked',
publicKey,
tweak,
)
expect(invoice).toBeDefined()
expect(invoice).toMatchObject({
invoice: expect.any(String),
operation_id: expect.any(String),
})
},
)

walletTest(
'scanReceivesForTweaks should return the operation id, ',
async ({ wallet }) => {
expect(wallet).toBeDefined()
expect(wallet.isOpen()).toBe(true)

// Make an ephemeral key pair
const { publicKey, secretKey } = keyPair()
const tweak = 1

// Create an invoice paying to the tweaked public key
const invoice = await wallet.lightning.createInvoiceTweaked(
1000,
'test tweaked',
publicKey,
tweak,
)
await expect(
wallet.testing.payWithFaucet(invoice.invoice),
).resolves.toBeDefined()

// Scan for the receive
const operationIds = await wallet.lightning.scanReceivesForTweaks(
secretKey,
[tweak],
{},
)
expect(operationIds).toBeDefined()
expect(operationIds).toHaveLength(1)

// Subscribe to claiming the receive
const subscription = await wallet.lightning.subscribeLnClaim(
operationIds[0],
(state) => {
expect(state).toBeDefined()
expect(state).toMatchObject({
state: 'claimed',
})
},
)
expect(subscription).toBeDefined()
},
)
99 changes: 63 additions & 36 deletions packages/core-web/src/services/LightningService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,69 +14,96 @@ import type {
export class LightningService {
constructor(private client: WorkerClient) {}

async createInvoiceWithGateway(
async createInvoice(
amount: MSats,
description: string,
expiryTime: number | null = null, // in seconds
extraMeta: JSONObject = {},
gatewayInfo: GatewayInfo,
) {
expiryTime?: number, // in seconds
extraMeta?: JSONObject,
gatewayInfo?: GatewayInfo,
): Promise<CreateBolt11Response> {
const gateway = gatewayInfo ?? (await this._getDefaultGatewayInfo())
return await this.client.rpcSingle('ln', 'create_bolt11_invoice', {
amount,
description,
expiry_time: expiryTime,
extra_meta: extraMeta,
gateway: gatewayInfo,
expiry_time: expiryTime ?? null,
extra_meta: extraMeta ?? {},
gateway,
})
}

async createInvoice(
async createInvoiceTweaked(
amount: MSats,
description: string,
expiryTime: number | null = null, // in seconds
extraMeta: JSONObject = {},
tweakKey: string,
index: number,
expiryTime?: number, // in seconds
gatewayInfo?: GatewayInfo,
extraMeta?: JSONObject,
): Promise<CreateBolt11Response> {
await this.updateGatewayCache()
const gateway = await this._getDefaultGatewayInfo()
return await this.client.rpcSingle('ln', 'create_bolt11_invoice', {
amount,
description,
expiry_time: expiryTime,
extra_meta: extraMeta,
gateway: gateway.info,
})
const gateway = gatewayInfo ?? (await this._getDefaultGatewayInfo())
return await this.client.rpcSingle(
'ln',
'create_bolt11_invoice_for_user_tweaked',
{
amount,
description,
expiry_time: expiryTime ?? null,
user_key: tweakKey,
index,
extra_meta: extraMeta ?? {},
gateway,
},
)
}

async payInvoiceWithGateway(
invoice: string,
gatewayInfo: GatewayInfo,
extraMeta: JSONObject = {},
) {
return await this.client.rpcSingle('ln', 'pay_bolt11_invoice', {
maybe_gateway: gatewayInfo,
invoice,
extra_meta: extraMeta,
// Returns the operation ids of payments received to the tweaks of the user secret key
async scanReceivesForTweaks(
tweakKey: string,
indices: number[],
extraMeta?: JSONObject,
): Promise<string[]> {
return await this.client.rpcSingle('ln', 'scan_receive_for_user_tweaked', {
user_key: tweakKey,
indices,
extra_meta: extraMeta ?? {},
})
}

private async _getDefaultGatewayInfo(): Promise<LightningGateway> {
private async _getDefaultGatewayInfo(): Promise<GatewayInfo> {
await this.updateGatewayCache()
const gateways = await this.listGateways()
return gateways[0]
return gateways[0]?.info
}

async payInvoice(
invoice: string,
extraMeta: JSONObject = {},
gatewayInfo?: GatewayInfo,
extraMeta?: JSONObject,
): Promise<OutgoingLightningPayment> {
await this.updateGatewayCache()
const gateway = await this._getDefaultGatewayInfo()
const gateway = gatewayInfo ?? (await this._getDefaultGatewayInfo())
return await this.client.rpcSingle('ln', 'pay_bolt11_invoice', {
maybe_gateway: gateway.info,
maybe_gateway: gateway,
invoice,
extra_meta: extraMeta,
extra_meta: extraMeta ?? {},
})
}

subscribeLnClaim(
operationId: string,
onSuccess: (state: LnReceiveState) => void = () => {},
onError: (error: string) => void = () => {},
) {
const unsubscribe = this.client.rpcStream(
'ln',
'subscribe_ln_claim',
{ operation_id: operationId },
onSuccess,
onError,
)

return unsubscribe
}

subscribeLnPay(
operationId: string,
onSuccess: (state: LnPayState) => void = () => {},
Expand Down
44 changes: 44 additions & 0 deletions packages/core-web/src/test/crypto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import * as secp256k1 from 'secp256k1'

const randomBytes = (size: number): Uint8Array => {
const array = new Uint8Array(size)
window.crypto.getRandomValues(array)
return array
}

interface KeyPair {
secretKey: string
publicKey: string
}

export const keyPair = (secretKey?: Uint8Array): KeyPair => {
const privateKey: Uint8Array = secretKey
? validatePrivateKey(secretKey)
: generatePrivateKey()

const publicKey = secp256k1.publicKeyCreate(privateKey)

return {
secretKey: Array.from(privateKey)
.map((b) => b.toString(16).padStart(2, '0'))
.join(''), // Convert Uint8Array to hex string
publicKey: Array.from(publicKey)
.map((b) => b.toString(16).padStart(2, '0'))
.join(''), // Convert Uint8Array to hex string
}
}

const validatePrivateKey = (key: Uint8Array): Uint8Array => {
if (!secp256k1.privateKeyVerify(key)) {
throw new Error('Invalid private key provided')
}
return key
}

const generatePrivateKey = (): Uint8Array => {
let key: Uint8Array
do {
key = randomBytes(32)
} while (!secp256k1.privateKeyVerify(key))
return key
}
Loading

0 comments on commit d0a7432

Please sign in to comment.