Skip to content

Commit

Permalink
feat(backend): add local payment (#2857)
Browse files Browse the repository at this point in the history
* feat(backend): add local payment quote migration

* feat(backend): WIP seperate ILPModels, LocalQuote, BaseQuote models

- trouble updating the services. this may not be the best way. also not sure I can prevent querying for non-local quotes on LocalQuote (and vice-versa for ILPQuote).
- the idea behind seperate models was a firm Quote type (it has the ilp specific props or it doesnt instead of  1 type MAYBE having them)
- typeguards could work instead but seemed messier. or maybe I can still have seperate quote service methods returning different types?

* refactor(backend): change model/services to reflect optional ilp quote details

- includes some WIP changes including gql field removal and handling missing ilp quote details

* feat(backend): WIP local payment method with getQuote

* feat(backend): add local payment method to payment method handler

* chore(backend): fix format

* feat(backend): stub in control payment handler service with receiver isLocal

- stubs in isLocal variable in place of actualy receiver.isLocal property
- updates/adds test. new test expectedly fails because there is no way to set the receiver to local yet. can implement after isLocal is added to receiver

* feat(backend): local payment .pay

* chore: rm comment

* chore: WIP debugging wrong sentAmount

- for new local payment method service, sentAmount matches debitAmount, whereas is matches receive amount for local/remote ilp payments

* chore: rm comment

* feat(backend): use receiver.isLocal to control payment method in quote/outgoing payment

* fix(backend): added source amount

* fix(backend): p2p case (cross currency, local, fixed send)

* fix: lint error

* chore: rm logs

* fix: quote service test

* fix: lint errors

* refactor(backend): split migrations

* refactor(backend): rm migration that was split into many

* WIP bruno requests for testing

* feat(backend): start rm ilpQuoteDetail join on op where not used

* fix(backend): rm unecessary ilpQuoteDetail join

* chore(backend): format

* fix(backend): dont join op on quote.ilpQuoteDetails on get

* fix(backend): rm ilpQuoteDetails join on op cancel

* fix(backend): rm unecessary join in op validate grant amount

* fix(backend): rm join from fundPayment

* fix(backend): rm unecessary join, unused method

* fix(backend): fetch ilpQuoteDetails where used instead of joining

* chore(backend): move ilpquotedetails dir

* chore(backend): rm console.log

* fix(backend): rm ilpQuoteDetails joins from quote service

* chore(backend): rm console.log

* refactor(backend): rename sourceAmount to debitAmountMinusFees

* chore(backend): cleanup, rm unused fee method

* test(backend): add local payment tests

* chore: format

* fix(bruno): local open payments requests

* test(backend): add integration tests for local payments

* chore(backend): cleanup

* chore: restore old version of date definition in test

* chore: cleanup

* fix: rm unused import

* test(integration): new case - p2p, fixed-send, local

* chore(integration): rename test for consistency

* fix(backend): throw error in pay if incoming payment is not pending

* feat(backend): simplify migrations

* chore(backend): clarify comment

* chore(auth): format

* refactor(backend): use IlpQuoteDetails model directly in ilp payment method

* refactor(backend): rm ilpQuoteDetails service

* fix(integration): wa typo

* chore: rm bruno test examples

* refactor: mv debitAmountMinusFees to fee calc and clarify TODO

* Update bruno/collections/Rafiki/Examples/Admin API -  only locally/Peer-to-Peer Local Payment/Create Receiver -remote Incoming Payment-.bru

Co-authored-by: Max Kurapov <max@interledger.org>

* fix: make timeout required again

Making optional depends on single phase transfer

* feat: error when post fails in local pay

* refactor(backend): add optional quoteId to getQuote args

- add runtime check to ilp payment method implementation requireing it

* refactor: rm ilp quote details out of quote service

* refactor: insert ilp quote details in ilp getQuote

* fix(backend): payment handler test

* chore(bruno): rename request

* chore(integration): rm erroneous todo comment

- comment indicated there was a bug where there was not.
incoming payment not completing was expected because it doesnt have an amount and didnt expire

* fix(backend): local quote amounts, estimatedExchangeRaet

* chore(backend): format

* refactor(backend): rate convert methods to be explicit

---------

Co-authored-by: Max Kurapov <max@interledger.org>
  • Loading branch information
BlairCurrey and mkurapov authored Nov 18, 2024
1 parent be426fa commit eae95ad
Show file tree
Hide file tree
Showing 38 changed files with 2,806 additions and 639 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
meta {
name: Create Outgoing Payment
type: graphql
seq: 3
}

post {
url: {{RafikiGraphqlHost}}/graphql
body: graphql
auth: none
}

body:graphql {
mutation CreateOutgoingPayment($input: CreateOutgoingPaymentInput!) {
createOutgoingPayment(input: $input) {
payment {
createdAt
error
metadata
id
walletAddressId
receiveAmount {
assetCode
assetScale
value
}
receiver
debitAmount {
assetCode
assetScale
value
}
sentAmount {
assetCode
assetScale
value
}
state
stateAttempts
}
}
}
}

body:graphql:vars {
{
"input": {
"walletAddressId": "{{gfranklinWalletAddressId}}",
"quoteId": "{{quoteId}}"
}
}
}

script:pre-request {
const scripts = require('./scripts');

scripts.addApiSignatureHeader();
}

script:post-response {
const body = res.getBody();

if (body?.data) {
bru.setEnvVar("outgoingPaymentId", body.data.createOutgoingPayment.payment.id);
}
}

tests {
test("Outgoing Payment id is string", function() {
expect(bru.getEnvVar("outgoingPaymentId")).to.be.a("string");
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
meta {
name: Create Quote
type: graphql
seq: 2
}

post {
url: {{RafikiGraphqlHost}}/graphql
body: graphql
auth: none
}

body:graphql {
mutation CreateQuote($input: CreateQuoteInput!) {
createQuote(input: $input) {
quote {
createdAt
expiresAt
id
walletAddressId
receiveAmount {
assetCode
assetScale
value
}
receiver
debitAmount {
assetCode
assetScale
value
}
}
}
}
}

body:graphql:vars {
{
"input": {
"walletAddressId": "{{gfranklinWalletAddressId}}",
"receiver": "{{receiverId}}"
}
}
}

script:pre-request {
const scripts = require('./scripts');

await scripts.loadWalletAddressIdsIntoVariables();

scripts.addApiSignatureHeader();
}

script:post-response {
const body = res.getBody();

if (body?.data) {
bru.setEnvVar("quoteId", body.data.createQuote.quote.id);
}
}

tests {
test("Quote id is string", function() {
expect(bru.getEnvVar("quoteId")).to.be.a("string");
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
meta {
name: Create Receiver -local Incoming Payment-
type: graphql
seq: 1
}

post {
url: {{RafikiGraphqlHost}}/graphql
body: graphql
auth: none
}

body:graphql {
mutation CreateReceiver($input: CreateReceiverInput!) {
createReceiver(input: $input) {
receiver {
completed
createdAt
expiresAt
metadata
id
incomingAmount {
assetCode
assetScale
value
}
walletAddressUrl
receivedAmount {
assetCode
assetScale
value
}
updatedAt
}
}
}
}

body:graphql:vars {
{
"input": {
"metadata": {
"description": "For lunch!"
},
"incomingAmount": {
"assetCode": "USD",
"assetScale": 2,
"value": 500
},
"walletAddressUrl": "https://cloud-nine-wallet-backend/accounts/bhamchest"
}
}
}

script:pre-request {
const scripts = require('./scripts');

scripts.addApiSignatureHeader();
}

script:post-response {
const body = res.getBody();

if (body?.data) {
bru.setEnvVar("receiverId", body.data.createReceiver.receiver.id);
}
}

tests {
test("Receiver id is string", function() {
expect(bru.getEnvVar("receiverId")).to.be.a("string");
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
meta {
name: Get Outgoing Payment
type: graphql
seq: 4
}

post {
url: {{RafikiGraphqlHost}}/graphql
body: graphql
auth: none
}

body:graphql {
query GetOutgoingPayment($id: String!) {
outgoingPayment(id: $id) {
createdAt
error
metadata
id
grantId
walletAddressId
quote {
id
}
receiveAmount {
assetCode
assetScale
value
}
receiver
debitAmount {
assetCode
assetScale
value
}
sentAmount {
assetCode
assetScale
value
}
state
stateAttempts
}
}
}

body:graphql:vars {
{
"id": "{{outgoingPaymentId}}"
}
}

script:pre-request {
const scripts = require('./scripts');

scripts.addApiSignatureHeader();
}
6 changes: 6 additions & 0 deletions localenv/cloud-nine-wallet/seed.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ accounts:
path: accounts/broke
brunoEnvVar: brokeWalletAddress
assetCode: USD
- name: "Luca Rossi"
id: 63dcc665-d946-4263-ac27-d0da1eb08a83
initialBalance: 50
path: accounts/lrossi
brunoEnvVar: lrossiWalletAddressId
assetCode: EUR
rates:
EUR:
MXN: 18.78
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/

exports.up = function (knex) {
return knex('quotes')
.whereNull('estimatedExchangeRate')
.update({
estimatedExchangeRate: knex.raw('?? / ??', [
'lowEstimatedExchangeRateNumerator',
'lowEstimatedExchangeRateDenominator'
])
})
.then(() => {
return knex.schema.table('quotes', (table) => {
table.decimal('estimatedExchangeRate', 20, 10).notNullable().alter()
})
})
}

/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function (knex) {
return knex.schema.table('quotes', (table) => {
table.decimal('estimatedExchangeRate', 20, 10).nullable().alter()
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function (knex) {
return (
knex.schema
// Create new table with columns from "quotes" to migrate
.createTable('ilpQuoteDetails', function (table) {
table.uuid('id').notNullable().primary()

// quoteId is purposefully not a FK referencing quote.id
// this allows us to create ilpQuoteDetail before quotes in service of
// fully decoupling payment method/quote services.
// https://github.com/interledger/rafiki/pull/2857#discussion_r1825891327
table.uuid('quoteId').notNullable().unique().index()

table.bigInteger('maxPacketAmount').notNullable()
table.decimal('minExchangeRateNumerator', 64, 0).notNullable()
table.decimal('minExchangeRateDenominator', 64, 0).notNullable()
table.decimal('lowEstimatedExchangeRateNumerator', 64, 0).notNullable()
table
.decimal('lowEstimatedExchangeRateDenominator', 64, 0)
.notNullable()
table.decimal('highEstimatedExchangeRateNumerator', 64, 0).notNullable()
table
.decimal('highEstimatedExchangeRateDenominator', 64, 0)
.notNullable()

table.timestamp('createdAt').defaultTo(knex.fn.now())
table.timestamp('updatedAt').defaultTo(knex.fn.now())
})
.then(() => {
return knex.raw(`
INSERT INTO "ilpQuoteDetails" (
id,
"quoteId",
"maxPacketAmount",
"minExchangeRateNumerator",
"minExchangeRateDenominator",
"lowEstimatedExchangeRateNumerator",
"lowEstimatedExchangeRateDenominator",
"highEstimatedExchangeRateNumerator",
"highEstimatedExchangeRateDenominator"
)
SELECT
gen_random_uuid(),
id AS "quoteId",
"maxPacketAmount",
"minExchangeRateNumerator",
"minExchangeRateDenominator",
"lowEstimatedExchangeRateNumerator",
"lowEstimatedExchangeRateDenominator",
"highEstimatedExchangeRateNumerator",
"highEstimatedExchangeRateDenominator"
FROM "quotes";
`)
})
)
}

/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function (knex) {
return knex.schema.dropTableIfExists('ilpQuoteDetails')
}
Loading

0 comments on commit eae95ad

Please sign in to comment.