Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/development' into melt-sign
Browse files Browse the repository at this point in the history
  • Loading branch information
lollerfirst committed Aug 12, 2024
2 parents 4ae5562 + ec9533b commit d5ec286
Show file tree
Hide file tree
Showing 11 changed files with 335 additions and 129 deletions.
35 changes: 31 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,42 @@ Go to the [docs](https://cashubtc.github.io/cashu-ts/docs) for detailed usage, o
npm i @cashu/cashu-ts
```

### Example
### Examples

#### Mint tokens

```typescript
import { CashuMint, CashuWallet, getEncodedToken } from '@cashu/cashu-ts';
import { CashuMint, CashuWallet, MintQuoteState } from '@cashu/cashu-ts';
const mintUrl = 'http://localhost:3338'; // the mint URL
const mint = new CashuMint(mintUrl);
const wallet = new CashuWallet(mint);
const mintQuote = await wallet.createMintQuote(64);
// pay the invoice here before you continue...
const mintQuoteChecked = await wallet.checkMintQuote(mintQuote.quote);
if (mintQuoteChecked.state == MintQuoteState.PAID) {
const { proofs } = await wallet.mintTokens(64, mintQuote.quote);
}
```

#### Melt tokens

```typescript
import { CashuMint, CashuWallet } from '@cashu/cashu-ts';
const mintUrl = 'http://localhost:3338'; // the mint URL
const mint = new CashuMint(mintUrl);
const wallet = new CashuWallet(mint);
const mintQuote = await wallet.mintQuote(64);
const tokens = await wallet.mintTokens(64, mintQuote.quote);

const invoice = 'lnbc......'; // Lightning invoice to pay
const meltQuote = await wallet.createMeltQuote(invoice);
const amountToSend = meltQuote.amount + meltQuote.fee_reserve;

// in a real wallet, we would coin select the correct amount of proofs from the wallet's storage
// instead of that, here we swap `proofs` with the mint to get the correct amount of proofs
const { returnChange: proofsToKeep, send: proofsToSend } = await wallet.send(amountToSend, proofs);
// store proofsToKeep in wallet ..

const meltResponse = await wallet.meltTokens(meltQuote, proofsToSend);
// store meltResponse.change in wallet ..
```

## Contribute
Expand Down
12 changes: 11 additions & 1 deletion migration-1.0.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Decoding LN invoices is no longer used inside the lib.

### `CashuWallet` interface changes

**`receive()` does no longer support multi-token tokens**
**`receive()` no longer supports multi-token tokens**

To reduce complexity, simplify error handling and to prepare for token V4, this feature has been removed. only the first token inside a token will be processed

Expand All @@ -42,6 +42,7 @@ type MintQuoteResponse = {
quote: string;
paid: boolean;
expiry: number;
state: MintQuoteState;
};
```

Expand All @@ -60,13 +61,22 @@ type MeltQuoteResponse = {
fee_reserve: number;
paid: boolean;
expiry: number;
payment_preimage: string;
state: MeltQuoteState;
change?: Array<SerializedBlindedSignature>;
};
```

where `quote` is the identifier to pass to `meltTokens()`

---

**`receive()`** and **`receiveTokenEntry()`** now return `Array<Proofs>`

where `Proofs` are the newly created `Proofs` from the received token. Will now throw an error instead of returning `proofsWithError`

---

### Model changes

**`MintKeys`--> `Keys`**:
Expand Down
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.12",
"version": "1.1.0-1",
"description": "cashu library for communicating with a cashu mint",
"main": "dist/lib/es5/index.js",
"module": "dist/lib/es6/index.js",
Expand Down
12 changes: 11 additions & 1 deletion src/base64.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ function encodeUint8toBase64(uint8array: Uint8Array): string {
return Buffer.from(uint8array).toString('base64');
}

function encodeUint8toBase64Url(bytes: Uint8Array): string {
return Buffer.from(bytes).toString('base64url').replace(/\=+$/, '');
}

function encodeBase64toUint8(base64String: string): Uint8Array {
return Buffer.from(base64String, 'base64');
}
Expand All @@ -29,4 +33,10 @@ function base64urlFromBase64(str: string) {
// .replace(/=/g, '.');
}

export { encodeUint8toBase64, encodeBase64toUint8, encodeJsonToBase64, encodeBase64ToJson };
export {
encodeUint8toBase64,
encodeUint8toBase64Url,
encodeBase64toUint8,
encodeJsonToBase64,
encodeBase64ToJson
};
67 changes: 62 additions & 5 deletions src/cbor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ function encodeItem(value: any, buffer: Array<number>) {
encodeString(value, buffer);
} else if (Array.isArray(value)) {
encodeArray(value, buffer);
} else if (value instanceof Uint8Array) {
encodeByteString(value, buffer);
} else if (typeof value === 'object') {
encodeObject(value, buffer);
} else {
Expand All @@ -38,16 +40,71 @@ function encodeUnsigned(value: number, buffer: Array<number>) {
}
}

function encodeByteString(value: Uint8Array, buffer: Array<number>) {
const length = value.length;

if (length < 24) {
buffer.push(0x40 + length);
} else if (length < 256) {
buffer.push(0x58, length);
} else if (length < 65536) {
buffer.push(0x59, (length >> 8) & 0xff, length & 0xff);
} else if (length < 4294967296) {
buffer.push(
0x5a,
(length >> 24) & 0xff,
(length >> 16) & 0xff,
(length >> 8) & 0xff,
length & 0xff
);
} else {
throw new Error('Byte string too long to encode');
}

for (let i = 0; i < value.length; i++) {
buffer.push(value[i]);
}
}

function encodeString(value: string, buffer: Array<number>) {
const utf8 = new TextEncoder().encode(value);
encodeUnsigned(utf8.length, buffer);
buffer[buffer.length - 1] |= 0x60;
utf8.forEach((b) => buffer.push(b));
const length = utf8.length;

if (length < 24) {
buffer.push(0x60 + length);
} else if (length < 256) {
buffer.push(0x78, length);
} else if (length < 65536) {
buffer.push(0x79, (length >> 8) & 0xff, length & 0xff);
} else if (length < 4294967296) {
buffer.push(
0x7a,
(length >> 24) & 0xff,
(length >> 16) & 0xff,
(length >> 8) & 0xff,
length & 0xff
);
} else {
throw new Error('String too long to encode');
}

for (let i = 0; i < utf8.length; i++) {
buffer.push(utf8[i]);
}
}

function encodeArray(value: Array<any>, buffer: Array<number>) {
encodeUnsigned(value.length, buffer);
buffer[buffer.length - 1] |= 0x80;
const length = value.length;
if (length < 24) {
buffer.push(0x80 | length);
} else if (length < 256) {
buffer.push(0x98, length);
} else if (length < 65536) {
buffer.push(0x99, length >> 8, length & 0xff);
} else {
throw new Error('Unsupported array length');
}

for (const item of value) {
encodeItem(item, buffer);
}
Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { CashuMint } from './CashuMint.js';
import { CashuWallet } from './CashuWallet.js';
import { setGlobalRequestOptions } from './request.js';
import { generateNewMnemonic, deriveSeedFromMnemonic } from '@cashu/crypto/modules/client/NUT09';
import { getEncodedToken, getDecodedToken, deriveKeysetId } from './utils.js';
import { getEncodedToken, getEncodedTokenV4, getDecodedToken, deriveKeysetId } from './utils.js';

export * from './model/types/index.js';

Expand All @@ -11,6 +11,7 @@ export {
CashuWallet,
getDecodedToken,
getEncodedToken,
getEncodedTokenV4,
deriveKeysetId,
generateNewMnemonic,
deriveSeedFromMnemonic,
Expand Down
18 changes: 18 additions & 0 deletions src/model/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -565,3 +565,21 @@ export type InvoiceData = {
memo?: string;
expiry?: number;
};

export type V4ProofTemplate = {
a: number;
s: string;
c: Uint8Array;
};

export type V4InnerToken = {
i: Uint8Array;
p: Array<V4ProofTemplate>;
};

export type TokenV4Template = {
t: Array<V4InnerToken>;
d: string;
m: string;
u: string;
};
73 changes: 66 additions & 7 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
import { encodeBase64ToJson, encodeBase64toUint8, encodeJsonToBase64 } from './base64.js';
import { AmountPreference, Keys, Proof, Token, TokenEntry, TokenV2 } from './model/types/index.js';
import {
encodeBase64ToJson,
encodeBase64toUint8,
encodeJsonToBase64,
encodeUint8toBase64,
encodeUint8toBase64Url
} from './base64.js';
import {
AmountPreference,
Keys,
Proof,
Token,
TokenEntry,
TokenV2,
TokenV4Template,
V4InnerToken,
V4ProofTemplate
} from './model/types/index.js';
import { TOKEN_PREFIX, TOKEN_VERSION } from './utils/Constants.js';
import { bytesToHex, hexToBytes } from '@noble/curves/abstract/utils';
import { sha256 } from '@noble/hashes/sha256';
import { decodeCBOR } from './cbor.js';
import { decodeCBOR, encodeCBOR } from './cbor.js';

function splitAmount(value: number, amountPreference?: Array<AmountPreference>): Array<number> {
const chunks: Array<number> = [];
Expand Down Expand Up @@ -77,6 +93,48 @@ function getEncodedToken(token: Token): string {
return TOKEN_PREFIX + TOKEN_VERSION + encodeJsonToBase64(token);
}

function getEncodedTokenV4(token: Token): string {
const idMap: { [id: string]: Array<Proof> } = {};
let mint: string | undefined = undefined;
for (let i = 0; i < token.token.length; i++) {
if (!mint) {
mint = token.token[i].mint;
} else {
if (mint !== token.token[i].mint) {
throw new Error('Multimint token can not be encoded as V4 token');
}
}
for (let j = 0; j < token.token[i].proofs.length; j++) {
const proof = token.token[i].proofs[j];
if (idMap[proof.id]) {
idMap[proof.id].push(proof);
} else {
idMap[proof.id] = [proof];
}
}
}
const tokenTemplate: TokenV4Template = {
m: mint,
u: token.unit || 'sat',
t: Object.keys(idMap).map(
(id): V4InnerToken => ({
i: hexToBytes(id),
p: idMap[id].map((p): V4ProofTemplate => ({ a: p.amount, s: p.secret, c: hexToBytes(p.C) }))
})
)
} as TokenV4Template;

if (token.memo) {
tokenTemplate.d = token.memo;
}

const encodedData = encodeCBOR(tokenTemplate);
const prefix = 'cashu';
const version = 'B';
const base64Data = encodeUint8toBase64Url(encodedData);
return prefix + version + base64Data;
}

/**
* Helper function to decode cashu tokens into object
* @param token an encoded cashu token (cashuAey...)
Expand Down Expand Up @@ -106,9 +164,10 @@ function handleTokens(token: string): Token {
} else if (version === 'B') {
const uInt8Token = encodeBase64toUint8(encodedToken);
const tokenData = decodeCBOR(uInt8Token) as {
t: { p: { a: number; s: string; c: Uint8Array }[]; i: Uint8Array }[];
t: Array<{ p: Array<{ a: number; s: string; c: Uint8Array }>; i: Uint8Array }>;
m: string;
d: string;
u: string;
};
const mergedTokenEntry: TokenEntry = { mint: tokenData.m, proofs: [] };
tokenData.t.forEach((tokenEntry) =>
Expand All @@ -121,10 +180,9 @@ function handleTokens(token: string): Token {
});
})
);
return { token: [mergedTokenEntry], memo: tokenData.d || '' };
} else {
throw new Error('Token version is not supported');
return { token: [mergedTokenEntry], memo: tokenData.d || '', unit: tokenData.u || 'sat' };
}
throw new Error('Token version is not supported');
}
/**
* Returns the keyset id of a set of keys
Expand Down Expand Up @@ -180,6 +238,7 @@ export {
bytesToNumber,
getDecodedToken,
getEncodedToken,
getEncodedTokenV4,
hexToNumber,
splitAmount,
getDefaultAmountPreference
Expand Down
Loading

0 comments on commit d5ec286

Please sign in to comment.