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/*'