Skip to content

Commit

Permalink
Merge branch 'v2.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
Egge21M committed Nov 26, 2024
2 parents d149123 + 4a7a76b commit 16e4827
Show file tree
Hide file tree
Showing 28 changed files with 2,927 additions and 874 deletions.
1 change: 0 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@

"rules": {
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/typedef": ["error", { "parameter": true, "arrowParameter": true }],
"@typescript-eslint/consistent-type-definitions": ["warn", "type"],
"@typescript-eslint/array-type": ["error", { "default": "generic" }],
"require-await": "off",
Expand Down
24 changes: 24 additions & 0 deletions .github/workflows/nextVersion.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Publish Package to npmjs
permissions:
contents: write
id-token: write
on:
push:
branches:
- staging
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
with:
node-version: 20
registry-url: 'https://registry.npmjs.org'
- run: npm i
- run: npm run compile
- run: npm version prerelease --preid=rc --no-git-tag-version
- run: git push
- run: npm publish --provenance --access public --tag next
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
2 changes: 1 addition & 1 deletion .github/workflows/nutshell-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
steps:
- name: Pull and start mint
run: |
docker run -d -p 3338:3338 --name nutshell -e MINT_LIGHTNING_BACKEND=FakeWallet -e MINT_LISTEN_HOST=0.0.0.0 -e MINT_LISTEN_PORT=3338 -e MINT_PRIVATE_KEY=TEST_PRIVATE_KEY cashubtc/nutshell:0.16.0 poetry run mint
docker run -d -p 3338:3338 --name nutshell -e MINT_LIGHTNING_BACKEND=FakeWallet -e MINT_INPUT_FEE_PPK=100 -e MINT_LISTEN_HOST=0.0.0.0 -e MINT_LISTEN_PORT=3338 -e MINT_PRIVATE_KEY=TEST_PRIVATE_KEY cashubtc/nutshell:0.16.2 poetry run mint
- name: Check running containers
run: docker ps
Expand Down
28 changes: 22 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,12 @@ 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);
await wallet.loadMint(); // persist wallet.keys and wallet.keysets to avoid calling loadMint() in the future
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);
const { proofs } = await wallet.mintProofs(64, mintQuote.quote);
}
```

Expand All @@ -77,21 +78,36 @@ if (mintQuoteChecked.state == MintQuoteState.PAID) {
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 wallet = new CashuWallet(mint); // load the keysets of the mint

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);
// CashuWallet.send performs coin selection and swaps the proofs with the mint
// if no appropriate amount can be selected offline. We must include potential
// ecash fees that the mint might require to melt the resulting proofsToSend later.
const { keep: proofsToKeep, send: proofsToSend } = await wallet.send(amountToSend, proofs, {
includeFees: true
});
// store proofsToKeep in wallet ..

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

#### Create a token and receive it

```typescript
// we assume that `wallet` already minted `proofs`, as above
const { keep, send } = await wallet.send(32, proofs);
const token = getEncodedTokenV4({ token: [{ mint: mintUrl, proofs: send }] });
console.log(token);

const wallet2 = new CashuWallet(mint); // receiving wallet
const receiveProofs = await wallet2.receive(token);
```

## Contribute

Contributions are very welcome.
Expand Down
235 changes: 235 additions & 0 deletions examples/simpleWallet_example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
import { CashuMint } from '../src/CashuMint.js';
import { CashuWallet } from '../src/CashuWallet.js';

import dns from 'node:dns';
import {
MeltQuoteResponse,
MeltQuoteState,
MintQuoteResponse,
MintQuoteState,
Proof,
Token
} from '../src/model/types/index.js';
import { getEncodedTokenV4, sumProofs } from '../src/utils.js';
dns.setDefaultResultOrder('ipv4first');

const externalInvoice =
'lnbc20u1p3u27nppp5pm074ffk6m42lvae8c6847z7xuvhyknwgkk7pzdce47grf2ksqwsdpv2phhwetjv4jzqcneypqyc6t8dp6xu6twva2xjuzzda6qcqzpgxqyz5vqsp5sw6n7cztudpl5m5jv3z6dtqpt2zhd3q6dwgftey9qxv09w82rgjq9qyyssqhtfl8wv7scwp5flqvmgjjh20nf6utvv5daw5h43h69yqfwjch7wnra3cn94qkscgewa33wvfh7guz76rzsfg9pwlk8mqd27wavf2udsq3yeuju';

// const mintUrl = 'https://testnut.cashu.space';
const mintUrl = 'http://localhost:3338';

// +++++++++++++++++++++ Example of a simple wallet implementation ++++++++++++++++++
// run the example with the following command: `npx examples/_simpleWallet.ts`
// a local mint instance should be running on port 3338. Startup command:
// docker run -d -p 3338:3338 --name nutshell -e MINT_LIGHTNING_BACKEND=FakeWallet -e MINT_INPUT_FEE_PPK=100 -e MINT_LISTEN_HOST=0.0.0.0 -e MINT_LISTEN_PORT=3338 -e MINT_PRIVATE_KEY=TEST_PRIVATE_KEY cashubtc/nutshell:0.16.0 poetry run mint

const mintAmount = 2050;

const runWalletExample = async () => {
try {
// instantiate a mint. It is used later by the CashuWallet to create api calls to the mint
const mint = new CashuMint(mintUrl);

// create a wallet with the keys loaded from the mint.
// The wallet is used as an interface for all cashu specific interactions
const wallet = new CashuWallet(mint);

// In order to load mint keys and other information about the mint, we should call this method
await wallet.loadMint();

//we can store the mint information, in case we later want to initialize the wallet without requesting the mint info again.
const mintInfo = wallet.mintInfo;
const keys = wallet.keys;
const keysets = wallet.keysets;

// ++++++++ Minting some ecash +++++++++++++

//First, let's decide on the amount of ecash we want to mint.

// next we create a place to store the ecash after it has been minted.
// Usually, this would be done in a database or somewhere where persistence is guaranteed.
// in this example, we will store the ecash in memory to make the example easier to understand.
let proofs: Proof[] = [];

// It's also a good idea to store proofs we've sent: in case the receiver does not claim them, we can receive them back later
let sentProofs: Proof[] = [];

// let's start with minting some ecash!
const mintEcash = async function () {
// with this command, we can initiate the creation of some ecash.
// The mint will return a request, that we have to fullfil in order for the ecash to be issued.
// (in most cases this will be a lightning invoice that needs to be paid)
console.log('Requesting a mint quote for' + mintAmount + 'satoshis.');
const quote = await wallet.createMintQuote(mintAmount);

console.log('Invoice to pay, in order to fullfill the quote: ' + quote.request);

//check if an error occurred in the creation of the quote
if (quote.error) {
console.error(quote.error, quote.code, quote.detail);
return;
}

// After some time of waiting, let's ask the mint if the request has been fullfilled.
setTimeout(async () => await checkMintQuote(quote), 1000);

const checkMintQuote = async (q: MintQuoteResponse) => {
// with this call, we can check the current status of a given quote
console.log('Checking the status of the quote: ' + q.quote);
const quote = await wallet.checkMintQuote(q.quote);
if (quote.error) {
console.error(quote.error, quote.code, quote.detail);
return;
}
if (quote.state === MintQuoteState.PAID) {
//if the quote was paid, we can ask the mint to issue the signatures for the ecash
const response = await wallet.mintProofs(mintAmount, quote.quote);
console.log(`minted proofs: ${response.map((p) => p.amount).join(', ')} sats`);

// let's store the proofs in the storage we previously created
proofs = response;

// after successfull minting, let's try to send some ecash
sendEcash(10);
} else if (quote.state === MintQuoteState.ISSUED) {
// if the quote has already been issued, we will receive an error if we try to mint again
console.error('Quote has already been issued');
return;
} else {
// if the quote has not yet been paid, we will wait some more to get the status of the quote again
setTimeout(async () => await checkMintQuote(q), 1000);
}
};
};
await mintEcash();

// ++++++++ Sending some ecash +++++++++++++

const sendEcash = async (amount: number) => {
// to send some ecash, we will call the `send` function
// e can provide the proofs we created in the previous step.
// If we provide too many proofs, they will be returned by the `keep` array
// after that,
// If the amount of the accumulated proofs we provide do not match exactly the amount we want to send,
// a split will have to be performed.
// this will burn the current proofs at the mint, and return a fresh set of proofs, matching the amount we want to send
const { keep, send } = await wallet.send(amount, proofs, { includeFees: true });

console.log(
`sending ${send.reduce((a, b) => a + b.amount, 0)} keeping ${keep.reduce(
(a, b) => a + b.amount,
0
)}`
);
// first, let's update our store with the new proofs
proofs = keep;

sentProofs.push(...send);

// and now, let's prepare the ecash we want to send as a cashu string
// For this, we can use the `Token` type
// In there, we set the mint url, and proof we want to send
const token: Token = {
mint: mintUrl,
proofs: send
};
// and finally, we can encode the token as a cashu string
const cashuString = getEncodedTokenV4(token);

// we can now send the cashu string to someone, and they can receive the ecash! let's try that next
console.log(cashuString);

// let's try to receive the cashu string back to ourselves
await receiveEcash(cashuString);
};

// ++++++++ Receiving some ecash +++++++++++++

const receiveEcash = async (cashuString: string) => {
// we can receive a cashu string back with the `receive` method
// this step is crucial. It will burn the received proofs, and create new ones,
// making sure the sender cannot try to double spend them.
const received = await wallet.receive(cashuString);
console.log('Received proofs:' + received.reduce((acc, proof) => acc + proof.amount, 0));

// after receiving, let's not forget to add the proofs back to our storage
proofs.push(...received);

// After receiving back our ecash, let's try to melt it.
// Melting ecash means, exchanging it back to the medium it was issued for.
// in most cases that would be lightning sats
await meltEcash();
};

// ++++++++ Melting some ecash +++++++++++++

const meltEcash = async () => {
// Similar to the minting process, we need to create a melt quote first.
// For this, we let the mint know what kind of request we want to be fulfilled.
// Usually this would be the payment of a lightning invoice.
const quote = await wallet.createMeltQuote(externalInvoice);

// After creating the melt quote, we can initiate the melting process.
const amountToMelt = quote.amount + quote.fee_reserve;

console.log(`quote amount: ${quote.amount}`);
console.log(`fee reserve proofs: ${quote.fee_reserve}`);
console.log(`Total quote amount: ${amountToMelt}`);

// in order to get the correct amount of proofs for the melt request, we can use the `send` function we used before
const { keep, send } = await wallet.send(amountToMelt, proofs, { includeFees: true });

// once again, we update the proofs we have to keep.
proofs = keep;

sentProofs.push(...send);

// and initiate the melting process with the prepared proofs.
const { change } = await wallet.meltProofs(quote, send);

//in case we overpaid for lightning fees, the mint will return the owed amount in ecash
proofs.push(...change);

if (quote.error) {
console.error(quote.error, quote.code, quote.detail);
return;
}

// After giving the mint some time to fullfil the melt request,
// we can check on the status
setTimeout(async () => await checkMeltQuote(quote), 1000);

const checkMeltQuote = async (q: MeltQuoteResponse) => {
// we can check on the status of the quote.
const quote = await wallet.checkMeltQuote(q.quote);

if (quote.error) {
console.error(quote.error, quote.code, quote.detail);
return;
}
if (quote.state === MeltQuoteState.PAID) {
// if the request has succeeded, we should receive the preimage for the paid invoice.
console.log(
'success! here is the payment preimage (if its null, the mints lightning backend did not forward the preimage): ',
quote.payment_preimage
);

console.log(`Ecash left: ${sumProofs(proofs)}`);
console.log(`Spent ecash notes: ${sumProofs(sentProofs)}`);

// +++++++++++++++++++ THE END +++++++++++++++++++
// There are more advanced features that were not touched on in this example.
// Take a look at the documentation to learn about features like seed recovery, locking ecash to pubkeys, etc.
} else {
// if the request has not succeeded, we will ask again
setTimeout(async () => await checkMeltQuote(quote), 1000);
}
};
};
} catch (error) {
console.error(error, 'u-oh something went wrong');
}
};

runWalletExample();
Loading

0 comments on commit 16e4827

Please sign in to comment.