Skip to content

Commit

Permalink
Merge pull request #148 from cashubtc/nut-05-add-state
Browse files Browse the repository at this point in the history
NUT-04 and NUT-05: Add `state` enum field
  • Loading branch information
callebtc authored Jul 16, 2024
2 parents c251e26 + ef8111f commit 15411d1
Show file tree
Hide file tree
Showing 10 changed files with 153 additions and 55 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@cashu/cashu-ts",
"version": "1.0.0-rc.8",
"version": "1.0.0-rc.9",
"description": "cashu library for communicating with a cashu mint",
"main": "dist/lib/es5/index.js",
"module": "dist/lib/es6/index.js",
Expand Down
44 changes: 32 additions & 12 deletions src/CashuMint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import type {
CheckStateResponse,
GetInfoResponse,
MeltPayload,
MeltResponse,
MintActiveKeys,
MintAllKeysets,
PostRestoreResponse,
Expand All @@ -18,9 +17,17 @@ import type {
MeltQuotePayload,
MeltQuoteResponse
} from './model/types/index.js';
import { MeltQuoteState } from './model/types/index.js';
import request from './request.js';
import { isObj, joinUrls, sanitizeUrl } from './utils.js';

import {
MeltQuoteResponsePaidDeprecated,
handleMeltQuoteResponseDeprecated
} from './legacy/nut-05.js';
import {
MintQuoteResponsePaidDeprecated,
handleMintQuoteResponseDeprecated
} from './legacy/nut-04.js';
/**
* Class represents Cashu Mint API. This class contains Lower level functions that are implemented by CashuWallet.
*/
Expand Down Expand Up @@ -104,11 +111,13 @@ class CashuMint {
customRequest?: typeof request
): Promise<MintQuoteResponse> {
const requestInstance = customRequest || request;
return requestInstance<MintQuoteResponse>({
const response = await requestInstance<MintQuoteResponse & MintQuoteResponsePaidDeprecated>({
endpoint: joinUrls(mintUrl, '/v1/mint/quote/bolt11'),
method: 'POST',
requestBody: mintQuotePayload
});
const data = handleMintQuoteResponseDeprecated(response);
return data;
}
/**
* Requests a new mint quote from the mint.
Expand All @@ -132,10 +141,13 @@ class CashuMint {
customRequest?: typeof request
): Promise<MintQuoteResponse> {
const requestInstance = customRequest || request;
return requestInstance<MintQuoteResponse>({
const response = await requestInstance<MintQuoteResponse & MintQuoteResponsePaidDeprecated>({
endpoint: joinUrls(mintUrl, '/v1/mint/quote/bolt11', quote),
method: 'GET'
});

const data = handleMintQuoteResponseDeprecated(response);
return data;
}
/**
* Gets an existing mint quote from the mint.
Expand Down Expand Up @@ -192,12 +204,14 @@ class CashuMint {
customRequest?: typeof request
): Promise<MeltQuoteResponse> {
const requestInstance = customRequest || request;
const data = await requestInstance<MeltQuoteResponse>({
const response = await requestInstance<MeltQuoteResponse & MeltQuoteResponsePaidDeprecated>({
endpoint: joinUrls(mintUrl, '/v1/melt/quote/bolt11'),
method: 'POST',
requestBody: meltQuotePayload
});

const data = handleMeltQuoteResponseDeprecated(response);

if (
!isObj(data) ||
typeof data?.amount !== 'number' ||
Expand Down Expand Up @@ -229,16 +243,20 @@ class CashuMint {
customRequest?: typeof request
): Promise<MeltQuoteResponse> {
const requestInstance = customRequest || request;
const data = await requestInstance<MeltQuoteResponse>({
const response = await requestInstance<MeltQuoteResponse & MeltQuoteResponsePaidDeprecated>({
endpoint: joinUrls(mintUrl, '/v1/melt/quote/bolt11', quote),
method: 'GET'
});

const data = handleMeltQuoteResponseDeprecated(response);

if (
!isObj(data) ||
typeof data?.amount !== 'number' ||
typeof data?.fee_reserve !== 'number' ||
typeof data?.quote !== 'string'
typeof data?.quote !== 'string' ||
typeof data?.state !== 'string' ||
!Object.values(MeltQuoteState).includes(data.state)
) {
throw new Error('bad response');
}
Expand All @@ -265,18 +283,20 @@ class CashuMint {
mintUrl: string,
meltPayload: MeltPayload,
customRequest?: typeof request
): Promise<MeltResponse> {
): Promise<MeltQuoteResponse> {
const requestInstance = customRequest || request;
const data = await requestInstance<MeltResponse>({
const response = await requestInstance<MeltQuoteResponse & MeltQuoteResponsePaidDeprecated>({
endpoint: joinUrls(mintUrl, '/v1/melt/bolt11'),
method: 'POST',
requestBody: meltPayload
});

const data = handleMeltQuoteResponseDeprecated(response);

if (
!isObj(data) ||
typeof data?.paid !== 'boolean' ||
(data?.payment_preimage !== null && typeof data?.payment_preimage !== 'string')
typeof data?.state !== 'string' ||
!Object.values(MeltQuoteState).includes(data.state)
) {
throw new Error('bad response');
}
Expand All @@ -288,7 +308,7 @@ class CashuMint {
* @param meltPayload
* @returns
*/
async melt(meltPayload: MeltPayload): Promise<MeltResponse> {
async melt(meltPayload: MeltPayload): Promise<MeltQuoteResponse> {
return CashuMint.melt(this._mintUrl, meltPayload, this._customRequest);
}
/**
Expand Down
5 changes: 3 additions & 2 deletions src/CashuWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ import {
type Token,
type TokenEntry,
CheckStateEnum,
SerializedBlindedSignature
SerializedBlindedSignature,
MeltQuoteState
} from './model/types/index.js';
import {
bytesToNumber,
Expand Down Expand Up @@ -439,7 +440,7 @@ class CashuWallet {
const meltResponse = await this.mint.melt(meltPayload);

return {
isPaid: meltResponse.paid ?? false,
isPaid: meltResponse.state === MeltQuoteState.PAID,
preimage: meltResponse.payment_preimage,
change: meltResponse?.change
? this.constructProofs(meltResponse.change, rs, secrets, keys)
Expand Down
4 changes: 2 additions & 2 deletions src/base64.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ function encodeBase64ToJson<T extends object>(base64String: string): T {
}

function base64urlToBase64(str: string) {
return str.replace(/-/g, '+').replace(/_/g, '/').split('=')[0]
return str.replace(/-/g, '+').replace(/_/g, '/').split('=')[0];
// .replace(/./g, '=');
}

function base64urlFromBase64(str: string) {
return str.replace(/\+/g, '-').replace(/\//g, '_').split('=')[0]
return str.replace(/\+/g, '-').replace(/\//g, '_').split('=')[0];
// .replace(/=/g, '.');
}

Expand Down
21 changes: 21 additions & 0 deletions src/legacy/nut-04.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { MintQuoteResponse } from '../model/types/index.js';
import { MintQuoteState } from '../model/types/index.js';

export type MintQuoteResponsePaidDeprecated = {
paid?: boolean;
};

export function handleMintQuoteResponseDeprecated(
response: MintQuoteResponse & MintQuoteResponsePaidDeprecated
): MintQuoteResponse {
// if the response MeltQuoteResponse has a "paid" flag, we monkey patch it to the state enum
if (!response.state) {
console.warn(
"Field 'state' not found in MintQuoteResponse. Update NUT-04 of mint: https://github.com/cashubtc/nuts/pull/141)"
);
if (typeof response.paid === 'boolean') {
response.state = response.paid ? MintQuoteState.PAID : MintQuoteState.UNPAID;
}
}
return response;
}
21 changes: 21 additions & 0 deletions src/legacy/nut-05.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { MeltQuoteResponse } from '../model/types/index.js';
import { MeltQuoteState } from '../model/types/index.js';

export type MeltQuoteResponsePaidDeprecated = {
paid?: boolean;
};

export function handleMeltQuoteResponseDeprecated(
response: MeltQuoteResponse & MeltQuoteResponsePaidDeprecated
): MeltQuoteResponse {
// if the response MeltQuoteResponse has a "paid" flag, we monkey patch it to the state enum
if (!response.state) {
console.warn(
"Field 'state' not found in MeltQuoteResponse. Update NUT-05 of mint: https://github.com/cashubtc/nuts/pull/136)"
);
if (typeof response.paid === 'boolean') {
response.state = response.paid ? MeltQuoteState.PAID : MeltQuoteState.UNPAID;
}
}
return response;
}
47 changes: 25 additions & 22 deletions src/model/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,12 @@ export type MeltQuotePayload = {
request: string;
};

export enum MeltQuoteState {
UNPAID = 'UNPAID',
PENDING = 'PENDING',
PAID = 'PAID'
}

/**
* Response from the mint after requesting a melt quote
*/
Expand All @@ -163,13 +169,21 @@ export type MeltQuoteResponse = {
*/
fee_reserve: number;
/**
* Whether the quote has been paid.
* State of the melt quote
*/
paid: boolean;
state: MeltQuoteState;
/**
* Timestamp of when the quote expires
*/
expiry: number;
/**
* preimage of the paid invoice. is null if it the invoice has not been paid yet. can be null, depending on which LN-backend the mint uses
*/
payment_preimage: string | null;
/**
* Return/Change from overpaid fees. This happens due to Lighting fee estimation being inaccurate
*/
change?: Array<SerializedBlindedSignature>;
} & ApiError;

/**
Expand All @@ -190,24 +204,6 @@ export type MeltPayload = {
outputs: Array<SerializedBlindedMessage>;
};

/**
* Response from the mint after paying a lightning invoice (melt)
*/
export type MeltResponse = {
/**
* if false, the proofs have not been invalidated and the payment can be tried later again with the same proofs
*/
paid: boolean;
/**
* preimage of the paid invoice. can be null, depending on which LN-backend the mint uses
*/
payment_preimage: string | null;
/**
* Return/Change from overpaid fees. This happens due to Lighting fee estimation being inaccurate
*/
change?: Array<SerializedBlindedSignature>;
} & ApiError;

/**
* Response after paying a Lightning invoice
*/
Expand Down Expand Up @@ -280,6 +276,13 @@ export type MintQuotePayload = {
*/
amount: number;
};

export enum MintQuoteState {
UNPAID = 'UNPAID',
PAID = 'PAID',
ISSUED = 'ISSUED'
}

/**
* Response from the mint after requesting a mint
*/
Expand All @@ -293,9 +296,9 @@ export type MintQuoteResponse = {
*/
quote: string;
/**
* Whether the quote has been paid.
* State of the mint quote
*/
paid: boolean;
state: MintQuoteState;
/**
* Timestamp of when the quote expires
*/
Expand Down
13 changes: 9 additions & 4 deletions test/request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import nock from 'nock';
import { CashuMint } from '../src/CashuMint.js';
import { CashuWallet } from '../src/CashuWallet.js';
import { setGlobalRequestOptions } from '../src/request.js';
import { MeltQuoteResponse } from '../src/model/types/index.js';

let request: Record<string, string> | undefined;
const mintUrl = 'https://localhost:3338';
Expand Down Expand Up @@ -29,8 +30,10 @@ describe('requests', () => {
return {
quote: 'test_melt_quote_id',
amount: 2000,
fee_reserve: 20
};
fee_reserve: 20,
payment_preimage: null,
state: 'UNPAID'
} as MeltQuoteResponse;
});

const wallet = new CashuWallet(mint, { unit });
Expand All @@ -49,8 +52,10 @@ describe('requests', () => {
return {
quote: 'test_melt_quote_id',
amount: 2000,
fee_reserve: 20
};
fee_reserve: 20,
payment_preimage: null,
state: 'UNPAID'
} as MeltQuoteResponse;
});

const wallet = new CashuWallet(mint, { unit });
Expand Down
Loading

0 comments on commit 15411d1

Please sign in to comment.