Skip to content

Commit

Permalink
feat: add option to create outgoing payment from incoming payment (#455)
Browse files Browse the repository at this point in the history
* feat: add option to create outgoing payment from incoming payment

changes outgoing payment body to accept incoming payment id
and debitAmount as alternative to quoteId
for creating outgoing payment

* chore: changeset

* refactor: rename incomingPaymentId to incomingPayment

* fix: rm redundant prettier config

* fix: prettier warnings
  • Loading branch information
BlairCurrey authored Apr 18, 2024
1 parent e6a2ed2 commit 3bbd59a
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 59 deletions.
5 changes: 5 additions & 0 deletions .changeset/friendly-dryers-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@interledger/open-payments': minor
---

changes POST /outgoing-payment body and client's outgoingPayment.create args to accept incomingPayment and debitAmount as alternative to quoteId. This supports creating outgoing payments directly from incoming payments instead of from a quote.
1 change: 0 additions & 1 deletion .prettierrc.yaml

This file was deleted.

7 changes: 1 addition & 6 deletions docs/src/content/docs/introduction/op-concepts.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,7 @@ When an `outgoing-payment` is completed against an open and active `incoming-pay

To set up ILP as a payment method in OP, the following are required:

- The <LinkOut href='https://interledger.org/developers/rfcs/ilp-addresses/'>ILP address
</LinkOut> of the payee’s ASE: The ILP address is required so that packets representing
payments routed over the Interledger network will be forwarded to the node owned
and operated by the intended receiver (i.e. payee's ASE). The `ilpAddress` field
must be sent by the payee’s ASE in the `methods` object on an `incoming-payment`
response.
- The <LinkOut href='https://interledger.org/developers/rfcs/ilp-addresses/'>ILP address </LinkOut> of the payee’s ASE: The ILP address is required so that packets representing payments routed over the Interledger network will be forwarded to the node owned and operated by the intended receiver (i.e. payee's ASE). The `ilpAddress` field must be sent by the payee’s ASE in the `methods` object on an `incoming-payment` response.
- Shared secret: A cryptographically secured secret exchanged between the sender and receiver to ensure that packets sent over the Interledger network through a <LinkOut href='https://interledger.org/developers/rfcs/stream-protocol/'>STREAM</LinkOut> connection can only be read by the two parties. The payee’s ASE must return the `sharedSecret` field in the `methods` object on an `incoming-payment` response.
- The payee's ASE must return the value `ilp` in the `type` field of the `methods` object on an `incoming-payment` response.
- The payer's ASE must send the value `ilp` in the `method` field when creating a `quote`.
65 changes: 49 additions & 16 deletions openapi/resource-server.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -291,27 +291,60 @@ paths:
quoteId: 'https://ilp.rafiki.money/quotes/ab03296b-0c8b-4776-b94e-7ee27d868d4d'
metadata:
externalRef: INV2022-02-0137
Create an outgoing payment based on an incoming payment:
value:
walletAddress: 'https://ilp.rafiki.money/alice/'
incomingPayment: 'https://ilp.rafiki.money/incoming-payments/8d4e4776-2e55-4e5a-bcbe-8348ed1e86de'
debitAmount:
value: '2500'
assetCode: USD
assetScale: 2
metadata:
externalRef: INV2022-02-0137
schema:
type: object
properties:
walletAddress:
$ref: ./schemas.yaml#/components/schemas/walletAddress
quoteId:
type: string
format: uri
description: The URL of the quote defining this payment's amounts.
metadata:
type: object
additionalProperties: true
description: Additional metadata associated with the outgoing payment. (Optional)
required:
- quoteId
- walletAddress
additionalProperties: false
oneOf:
- type: object
properties:
walletAddress:
$ref: ./schemas.yaml#/components/schemas/walletAddress
quoteId:
type: string
format: uri
description: The URL of the quote defining this payment's amounts.
metadata:
type: object
additionalProperties: true
description: Additional metadata associated with the outgoing payment. (Optional)
required:
- quoteId
- walletAddress
additionalProperties: false
- type: object
properties:
walletAddress:
$ref: ./schemas.yaml#/components/schemas/walletAddress
incomingPayment:
type: string
format: uri
description: The URL of the incoming payment this outgoing payment will fulfill.
debitAmount:
description: The fixed amount that would be sent from the sending wallet address given a successful outgoing payment.
$ref: ./schemas.yaml#/components/schemas/amount
metadata:
type: object
additionalProperties: true
description: Additional metadata associated with the outgoing payment. (Optional)
required:
- incomingPayment
- debitAmount
- walletAddress
additionalProperties: false
description: |-
A subset of the outgoing payments schema is accepted as input to create a new outgoing payment.
The `debitAmount` must use the same `assetCode` and `assetScale` as the wallet address.
Either provide a `quoteId` to create an outgoing payment based on a quote or provide `incomingPayment` and `debitAmount` to create an outgoing payment directly from an incoming payment.
required: true
description: |-
An **outgoing payment** is a sub-resource of a wallet address. It represents a payment from the wallet address.
Expand Down
73 changes: 47 additions & 26 deletions packages/open-payments/src/client/outgoing-payment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import * as requestors from './requests'
import { OpenPaymentsClientError } from './error'
import assert from 'assert'
import { getResourceServerOpenAPI } from '../openapi'
import { CreateOutgoingPaymentArgs } from '../types'

jest.mock('./requests', () => {
return {
Expand Down Expand Up @@ -280,38 +281,58 @@ describe('outgoing-payment', (): void => {
})

describe('createOutgoingPayment', (): void => {
const quoteId = `${serverAddress}/quotes/${uuid()}`
const quoteId_ = `${serverAddress}/quotes/${uuid()}`
const incomingPayment = `${serverAddress}/incoming-payments/${uuid()}`
const debitAmount = {
value: '2500',
assetCode: 'USD',
assetScale: 2
}

test.each`
metadata
${{ description: 'Some description', externalRef: '#INV-1' }}
${undefined}
`('creates outgoing payment', async ({ metadata }): Promise<void> => {
const outgoingPayment = mockOutgoingPayment({
quoteId | incomingPayment | debitAmount | metadata | createSource
${quoteId_} | ${undefined} | ${undefined} | ${{ description: 'Some description', externalRef: '#INV-1' }} | ${'quote'}
${quoteId_} | ${undefined} | ${undefined} | ${undefined} | ${'quote'}
${undefined} | ${incomingPayment} | ${debitAmount} | ${undefined} | ${'incoming payment'}
`(
'creates outgoing payment from $createSource',
async ({
quoteId,
incomingPayment,
debitAmount,
metadata
})
}): Promise<void> => {
// quoteId and incomingPayment/debitAmount should be mutually exclusive
assert(quoteId || (incomingPayment && debitAmount))
assert(!(quoteId && incomingPayment))
assert(!(quoteId && debitAmount))

const outgoingPayment = mockOutgoingPayment({
quoteId: quoteId || uuid(),
metadata
})

const scope = nock(serverAddress)
.post('/outgoing-payments')
.reply(200, outgoingPayment)
const scope = nock(serverAddress)
.post('/outgoing-payments')
.reply(200, outgoingPayment)

const result = await createOutgoingPayment(
deps,
{
url: serverAddress,
accessToken: 'accessToken'
},
openApiValidators.successfulValidator,
{
quoteId,
metadata,
walletAddress
}
)
expect(result).toEqual(outgoingPayment)
scope.done()
})
const createOutgoingPaymentInput: CreateOutgoingPaymentArgs = quoteId
? { walletAddress, metadata, quoteId }
: { walletAddress, metadata, incomingPayment, debitAmount }

const result = await createOutgoingPayment(
deps,
{
url: serverAddress,
accessToken: 'accessToken'
},
openApiValidators.successfulValidator,
createOutgoingPaymentInput
)
expect(result).toEqual(outgoingPayment)
scope.done()
}
)

test('throws if outgoing payment does not pass validation', async (): Promise<void> => {
const outgoingPayment = mockOutgoingPayment({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -416,19 +416,34 @@ export interface operations {
* A subset of the outgoing payments schema is accepted as input to create a new outgoing payment.
*
* The `debitAmount` must use the same `assetCode` and `assetScale` as the wallet address.
*
* Either provide a `quoteId` to create an outgoing payment based on a quote or provide `incomingPayment` and `debitAmount` to create an outgoing payment directly from an incoming payment.
*/
requestBody: {
content: {
"application/json": {
walletAddress: external["schemas.yaml"]["components"]["schemas"]["walletAddress"];
/**
* Format: uri
* @description The URL of the quote defining this payment's amounts.
*/
quoteId: string;
/** @description Additional metadata associated with the outgoing payment. (Optional) */
metadata?: { [key: string]: unknown };
};
"application/json":
| {
walletAddress: external["schemas.yaml"]["components"]["schemas"]["walletAddress"];
/**
* Format: uri
* @description The URL of the quote defining this payment's amounts.
*/
quoteId: string;
/** @description Additional metadata associated with the outgoing payment. (Optional) */
metadata?: { [key: string]: unknown };
}
| {
walletAddress: external["schemas.yaml"]["components"]["schemas"]["walletAddress"];
/**
* Format: uri
* @description The URL of the incoming payment this outgoing payment will fulfill.
*/
incomingPayment: string;
/** @description The fixed amount that would be sent from the sending wallet address given a successful outgoing payment. */
debitAmount: external["schemas.yaml"]["components"]["schemas"]["amount"];
/** @description Additional metadata associated with the outgoing payment. (Optional) */
metadata?: { [key: string]: unknown };
};
};
};
};
Expand Down

0 comments on commit 3bbd59a

Please sign in to comment.