From cd0b690b972fcf44cbfe01c72a825af0b2a67246 Mon Sep 17 00:00:00 2001 From: Max Kurapov Date: Thu, 4 Sep 2025 19:33:05 +0200 Subject: [PATCH 1/2] feat(example): add peer-to-peer example --- .gitignore | 84 +---------- examples/peer-to-peer/README.md | 20 +++ examples/peer-to-peer/index.js | 231 +++++++++++++++++++++++++++++ examples/peer-to-peer/package.json | 18 +++ pnpm-lock.yaml | 13 ++ pnpm-workspace.yaml | 2 +- 6 files changed, 284 insertions(+), 84 deletions(-) create mode 100644 examples/peer-to-peer/README.md create mode 100644 examples/peer-to-peer/index.js create mode 100644 examples/peer-to-peer/package.json diff --git a/.gitignore b/.gitignore index 3767965..0e7be45 100644 --- a/.gitignore +++ b/.gitignore @@ -41,86 +41,4 @@ dist **/.terraform tmp -.spectral.json -styles/.vale-config/3-MDX.ini -styles/Google/Acronyms.yml -styles/Google/AMPM.yml -styles/Google/Colons.yml -styles/Google/Contractions.yml -styles/Google/DateFormat.yml -styles/Google/Ellipses.yml -styles/Google/EmDash.yml -styles/Google/Exclamation.yml -styles/Google/FirstPerson.yml -styles/Google/Gender.yml -styles/Google/GenderBias.yml -styles/Google/HeadingPunctuation.yml -styles/Google/Headings.yml -styles/Google/Latin.yml -styles/Google/LyHyphens.yml -styles/Google/meta.json -styles/Google/OptionalPlurals.yml -styles/Google/Ordinal.yml -styles/Google/OxfordComma.yml -styles/Google/Parens.yml -styles/Google/Passive.yml -styles/Google/Periods.yml -styles/Google/Quotes.yml -styles/Google/Ranges.yml -styles/Google/Semicolons.yml -styles/Google/Slang.yml -styles/Google/Spacing.yml -styles/Google/Spelling.yml -styles/Google/Units.yml -styles/Google/vocab.txt -styles/Google/We.yml -styles/Google/Will.yml -styles/Google/WordList.yml -styles/proselint/Airlinese.yml -styles/proselint/AnimalLabels.yml -styles/proselint/Annotations.yml -styles/proselint/Apologizing.yml -styles/proselint/Archaisms.yml -styles/proselint/But.yml -styles/proselint/Cliches.yml -styles/proselint/CorporateSpeak.yml -styles/proselint/Currency.yml -styles/proselint/Cursing.yml -styles/proselint/DateCase.yml -styles/proselint/DateMidnight.yml -styles/proselint/DateRedundancy.yml -styles/proselint/DateSpacing.yml -styles/proselint/DenizenLabels.yml -styles/proselint/Diacritical.yml -styles/proselint/GenderBias.yml -styles/proselint/GroupTerms.yml -styles/proselint/Hedging.yml -styles/proselint/Hyperbole.yml -styles/proselint/Jargon.yml -styles/proselint/LGBTOffensive.yml -styles/proselint/LGBTTerms.yml -styles/proselint/Malapropisms.yml -styles/proselint/meta.json -styles/proselint/Needless.yml -styles/proselint/Nonwords.yml -styles/proselint/Oxymorons.yml -styles/proselint/P-Value.yml -styles/proselint/RASSyndrome.yml -styles/proselint/README.md -styles/proselint/Skunked.yml -styles/proselint/Spelling.yml -styles/proselint/Typography.yml -styles/proselint/Uncomparables.yml -styles/proselint/Very.yml -styles/write-good/Cliches.yml -styles/write-good/E-Prime.yml -styles/write-good/Illusions.yml -styles/write-good/meta.json -styles/write-good/Passive.yml -styles/write-good/README.md -styles/write-good/So.yml -styles/write-good/ThereIs.yml -styles/write-good/TooWordy.yml -styles/write-good/Weasel.yml -.vale.ini -vale +*.key \ No newline at end of file diff --git a/examples/peer-to-peer/README.md b/examples/peer-to-peer/README.md new file mode 100644 index 0000000..4e968ab --- /dev/null +++ b/examples/peer-to-peer/README.md @@ -0,0 +1,20 @@ +# Peer-to-Peer Payment Example + +This script sends money between two wallet addresses using the [Node Open Payments client](https://github.com/interledger/open-payments-node/tree/main/packages/open-payments). + +The script creates an incoming payment on the receiving wallet address, and a quote on the sending wallet address (after getting grants for both). It also creates an interactive outgoing payment grant, which will require user interaction. + +The script then finalizes the grant (after it's accepted interactively via the browser), and creates the outgoing payment. + +### Steps + +1. Make sure you have NodeJS installed +2. Run `pnpm install` +3. Get a private key, client wallet address and keyId from an Open Payments enabled wallet, and add them in the script. + +> You can use our [test wallet](https://wallet.interledger-test.dev) to create accounts, and generate developer keys for making payments via the Open Payments APIs. Instructions about how to use the test wallet are found at the [Open Payments API documentation](https://openpayments.dev/sdk/before-you-begin/). + +4. Pick a receiving wallet address, a sending wallet address, and add them as variables in the script. +5. Run `node index.js` +6. Click on the outputted URL, to accept the outgoing payment grant. +7. Return to the terminal, hit enter. After this, the script will create the outgoing payment and funds will move between the receiver and the sender! diff --git a/examples/peer-to-peer/index.js b/examples/peer-to-peer/index.js new file mode 100644 index 0000000..daf9a39 --- /dev/null +++ b/examples/peer-to-peer/index.js @@ -0,0 +1,231 @@ +/** + * This script sets up an incoming payment on a receiving wallet address, + * and a quote on the sending wallet address (after getting grants for both of the resources). + * + * The final step is asking for an outgoing payment grant for the sending wallet address. + * Since this needs user interaction, you will need to navigate to the URL, and accept the interactive grant. + * + * To start, please add the variables for configuring the client & the wallet addresses for the payment. + */ + +import { + createAuthenticatedClient, + OpenPaymentsClientError, + isFinalizedGrant +} from '@interledger/open-payments' +import readline from 'readline/promises' +;(async () => { + // Client configuration + const PRIVATE_KEY_PATH = 'private.key' + const KEY_ID = '' + + // Make sure the wallet addresses starts with https:// (not $) + const CLIENT_WALLET_ADDRESS_URL = '' + const SENDING_WALLET_ADDRESS_URL = '' + const RECEIVING_WALLET_ADDRESS_URL = '' + + const client = await createAuthenticatedClient({ + walletAddressUrl: CLIENT_WALLET_ADDRESS_URL, + keyId: KEY_ID, + privateKey: PRIVATE_KEY_PATH + }) + + // Step 1: Get the sending and receiving wallet addresses + const sendingWalletAddress = await client.walletAddress.get({ + url: SENDING_WALLET_ADDRESS_URL + }) + const receivingWalletAddress = await client.walletAddress.get({ + url: RECEIVING_WALLET_ADDRESS_URL + }) + + console.log('\nStep 1: got wallet addresses', { + receivingWalletAddress, + sendingWalletAddress + }) + + // Step 2: Get a grant for the incoming payment, so we can create the incoming payment on the receiving wallet address + const incomingPaymentGrant = await client.grant.request( + { + url: receivingWalletAddress.authServer + }, + { + access_token: { + access: [ + { + type: 'incoming-payment', + actions: ['read', 'complete', 'create'] + } + ] + } + } + ) + + console.log( + '\nStep 2: got incoming payment grant for receiving wallet address', + incomingPaymentGrant + ) + + if (!isFinalizedGrant(incomingPaymentGrant)) { + throw new Error('Expected finalized incoming payment grant') + } + + // Step 3: Create the incoming payment. This will be where funds will be received. + const incomingPayment = await client.incomingPayment.create( + { + url: receivingWalletAddress.resourceServer, + accessToken: incomingPaymentGrant.access_token.value + }, + { + walletAddress: receivingWalletAddress.id, + incomingAmount: { + assetCode: receivingWalletAddress.assetCode, + assetScale: receivingWalletAddress.assetScale, + value: '1000' + } + } + ) + + console.log( + '\nStep 3: created incoming payment on receiving wallet address', + incomingPayment + ) + + // Step 4: Get a quote grant, so we can create a quote on the sending wallet address + const quoteGrant = await client.grant.request( + { + url: sendingWalletAddress.authServer + }, + { + access_token: { + access: [ + { + type: 'quote', + actions: ['create', 'read'] + } + ] + } + } + ) + + if (!isFinalizedGrant(quoteGrant)) { + throw new Error('Expected finalized quote grant') + } + + console.log('\nStep 4: got quote grant on sending wallet address', quoteGrant) + + // Step 5: Create a quote, this gives an indication of how much it will cost to pay into the incoming payment + const quote = await client.quote.create( + { + url: sendingWalletAddress.resourceServer, + accessToken: quoteGrant.access_token.value + }, + { + walletAddress: sendingWalletAddress.id, + receiver: incomingPayment.id, + method: 'ilp' + } + ) + + console.log('\nStep 5: got quote on sending wallet address', quote) + + // Step 7: Start the grant process for the outgoing payments. + // This is an interactive grant: the user (in this case, you) will need to accept the grant by navigating to the outputted link. + const outgoingPaymentGrant = await client.grant.request( + { + url: sendingWalletAddress.authServer + }, + { + access_token: { + access: [ + { + type: 'outgoing-payment', + actions: ['read', 'create'], + limits: { + debitAmount: { + assetCode: quote.debitAmount.assetCode, + assetScale: quote.debitAmount.assetScale, + value: quote.debitAmount.value + } + }, + identifier: sendingWalletAddress.id + } + ] + }, + interact: { + start: ['redirect'] + // finish: { + // method: "redirect", + // // This is where you can (optionally) redirect a user to after going through interaction. + // // Keep in mind, you will need to parse the interact_ref in the resulting interaction URL, + // // and pass it into the grant continuation request. + // uri: "https://example.com", + // nonce: crypto.randomUUID(), + // }, + } + } + ) + + console.log( + '\nStep 7: got pending outgoing payment grant', + outgoingPaymentGrant + ) + console.log( + 'Please navigate to the following URL, to accept the interaction from the sending wallet:' + ) + console.log(outgoingPaymentGrant.interact.redirect) + + await readline + .createInterface({ input: process.stdin, output: process.stdout }) + .question('\nPlease accept grant and press enter...') + + let finalizedOutgoingPaymentGrant + + const grantContinuationErrorMessage = + '\nThere was an error continuing the grant. You probably have not accepted the grant at the url (or it has already been used up, in which case, rerun the script).' + + try { + finalizedOutgoingPaymentGrant = await client.grant.continue({ + url: outgoingPaymentGrant.continue.uri, + accessToken: outgoingPaymentGrant.continue.access_token.value + }) + } catch (err) { + if (err instanceof OpenPaymentsClientError) { + console.log(grantContinuationErrorMessage) + process.exit() + } + + throw err + } + + if (!isFinalizedGrant(finalizedOutgoingPaymentGrant)) { + console.log( + 'There was an error continuing the grant. You probably have not accepted the grant at the url.' + ) + process.exit() + } + + console.log( + '\nStep 6: got finalized outgoing payment grant', + finalizedOutgoingPaymentGrant + ) + + // Step 7: Finally, create the outgoing payment on the sending wallet address. + // This will make a payment from the outgoing payment to the incoming one (over ILP) + const outgoingPayment = await client.outgoingPayment.create( + { + url: sendingWalletAddress.resourceServer, + accessToken: finalizedOutgoingPaymentGrant.access_token.value + }, + { + walletAddress: sendingWalletAddress.id, + quoteId: quote.id + } + ) + + console.log( + '\nStep 7: Created outgoing payment. Funds will now move from the outgoing payment to the incoming payment.', + outgoingPayment + ) + + process.exit() +})() diff --git a/examples/peer-to-peer/package.json b/examples/peer-to-peer/package.json new file mode 100644 index 0000000..9ced9ea --- /dev/null +++ b/examples/peer-to-peer/package.json @@ -0,0 +1,18 @@ +{ + "name": "example-peer-to-peer", + "private": "true", + "version": "1.0.0", + "description": "", + "main": "index.js", + "type": "module", + "scripts": { + "start": "node index.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@interledger/open-payments": "workspace:^", + "readline": "^1.3.0" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4d2049f..b9c14d7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -51,6 +51,15 @@ importers: specifier: ^5.8.2 version: 5.8.2 + examples/peer-to-peer: + dependencies: + '@interledger/open-payments': + specifier: workspace:^ + version: link:../../packages/open-payments + readline: + specifier: ^1.3.0 + version: 1.3.0 + packages/http-signature-utils: dependencies: http-message-signatures: @@ -5320,6 +5329,10 @@ packages: picomatch: 2.3.1 dev: true + /readline@1.3.0: + resolution: {integrity: sha512-k2d6ACCkiNYz222Fs/iNze30rRJ1iIicW7JuX/7/cozvih6YCkFZH+J6mAFDVgv0dRBaAyr4jDqC95R2y4IADg==} + dev: false + /real-require@0.2.0: resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} engines: {node: '>= 12.13.0'} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 59a60bd..1598933 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,3 @@ packages: - 'packages/*' - - 'docs' + - 'examples/*' From df3f14ae13099f6d4437878d37dd5a0aad54ac15 Mon Sep 17 00:00:00 2001 From: Max Kurapov Date: Wed, 10 Sep 2025 12:03:45 +0200 Subject: [PATCH 2/2] docs: add tutorial video to README --- examples/peer-to-peer/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/examples/peer-to-peer/README.md b/examples/peer-to-peer/README.md index 4e968ab..4c34bb2 100644 --- a/examples/peer-to-peer/README.md +++ b/examples/peer-to-peer/README.md @@ -18,3 +18,7 @@ The script then finalizes the grant (after it's accepted interactively via the b 5. Run `node index.js` 6. Click on the outputted URL, to accept the outgoing payment grant. 7. Return to the terminal, hit enter. After this, the script will create the outgoing payment and funds will move between the receiver and the sender! + +### Tutorial Video + +[![Open Payments Client Tutorial](https://img.youtube.com/vi/N9ggNr23FYc/0.jpg)](https://www.youtube.com/watch?v=N9ggNr23FYc)