diff --git a/package.json b/package.json index 0b4ad73..49b076b 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "example-create-issuer": "npm run _debug -- dist/example/create-issuer.js", "example-issue-credential": "npm run _debug -- dist/example/issue-credential.js", "example-e2e-exchange": "npm run _debug -- dist/example/full-tbdex-exchange.js", + "example-stored-balance": "npm run _debug -- dist/example/full-stored-balances.js", "lint": "eslint . --ext .ts --max-warnings 0", "lint:fix": "eslint . --ext .ts --fix", "seed-offerings": "npm run _start -- dist/seed-offerings.js", diff --git a/src/db/balances-repository.ts b/src/db/balances-repository.ts index 4b8c7ef..511b2e8 100644 --- a/src/db/balances-repository.ts +++ b/src/db/balances-repository.ts @@ -1,14 +1,16 @@ -import type { BalancesApi } from '@tbdex/http-server' +import type { BalancesApi, Quote } from '@tbdex/http-server' import { Balance } from '@tbdex/http-server' import { config } from '../config.js' +let available = '1000' + export class _BalancesRepository implements BalancesApi { async getBalances({ requesterDid }): Promise { console.log('getBalances for:', requesterDid) const bal = Balance.create({ data: { currencyCode: 'USDC', - available: '1000', + available: available, }, metadata: { from: config.pfiDid.uri, @@ -16,6 +18,16 @@ export class _BalancesRepository implements BalancesApi { }) return [bal] } + + withdraw(quote: Quote) { + // substract this from available + available = (parseFloat(available) - parseFloat(quote.data.payout.amount)).toString() + } + + deposit(quote: Quote) { + // add this to available + available = (parseFloat(available) + parseFloat(quote.data.payin.amount)).toString() + } } export const BalancesRepository = new _BalancesRepository() \ No newline at end of file diff --git a/src/db/exchange-repository.ts b/src/db/exchange-repository.ts index c5753a0..92d3162 100644 --- a/src/db/exchange-repository.ts +++ b/src/db/exchange-repository.ts @@ -1,7 +1,9 @@ -import { Message, Close, Order, OrderStatus, Quote, ExchangesApi, Rfq, Parser, Exchange } from '@tbdex/http-server' +import { Message, Close, Order, OrderStatus, Quote, ExchangesApi, Rfq, Parser, Exchange, Offering } from '@tbdex/http-server' import type { MessageModel, MessageKind, GetExchangesFilter } from '@tbdex/http-server' +import { OfferingRepository } from './offering-repository.js' import { Postgres } from './postgres.js' import { config } from '../config.js' +import { BalancesRepository } from './balances-repository.js' class _ExchangeRepository implements ExchangesApi { @@ -77,6 +79,8 @@ class _ExchangeRepository implements ExchangesApi { return await this.getMessage({ exchangeId: opts.exchangeId, messageKind: 'rfq' }) as Rfq } + + async getQuote(opts: { exchangeId: string }): Promise { return await this.getMessage({ exchangeId: opts.exchangeId, messageKind: 'quote' }) as Quote } @@ -123,6 +127,7 @@ class _ExchangeRepository implements ExchangesApi { } } + async addMessage(opts: { message: Message }) { const { message } = opts const subject = aliceMessageKinds.has(message.kind) ? message.from : message.to @@ -140,25 +145,17 @@ class _ExchangeRepository implements ExchangesApi { console.log(`Add ${message.kind} Result: ${JSON.stringify(result, null, 2)}`) if (message.kind == 'rfq') { - const quote = Quote.create({ - metadata: { - from: config.pfiDid.uri, - to: message.from, - exchangeId: message.exchangeId - }, - data: { - expiresAt: new Date(new Date().getTime() + 60 * 60000).toISOString(), - payin: { - currencyCode: 'BTC', - amount: '1000.00' - }, - payout: { - currencyCode: 'KES', - amount: '123456789.00' - } - } - }) - await quote.sign(config.pfiDid) + const rfq = message as Rfq + let quote: Quote + if (rfq.data.payin.kind == 'STORED_BALANCE' && rfq.data.payout.kind == 'WIRE_TRANSFER') { + quote = await this.withdrawalQuote(rfq) + } + if (rfq.data.payin.kind == 'WIRE_TRANSFER' && rfq.data.payout.kind == 'STORED_BALANCE') { + quote = await this.depositQuote(rfq) + } + else { + quote = await this.BtcKesQuote(rfq) + } this.addMessage({ message: quote as Quote}) } @@ -176,6 +173,13 @@ class _ExchangeRepository implements ExchangesApi { await orderStatus.sign(config.pfiDid) this.addMessage({ message: orderStatus as OrderStatus}) + const quote = await this.getQuote({ exchangeId: message.exchangeId }) + if (quote.data.payout.currencyCode == 'STORED_BALANCE') { + BalancesRepository.deposit(quote) + } else if (quote.data.payin.currencyCode == 'STORED_BALANCE') { + BalancesRepository.withdraw(quote) + } + await new Promise(resolve => setTimeout(resolve, 1000)) // 1 second delay orderStatus = OrderStatus.create({ @@ -207,6 +211,77 @@ class _ExchangeRepository implements ExchangesApi { this.addMessage({ message: close as Close }) } } + + + private async depositQuote(rfq: Rfq) { + const quote = Quote.create({ + metadata: { + from: config.pfiDid.uri, + to: rfq.from, + exchangeId: rfq.exchangeId + }, + data: { + expiresAt: new Date(new Date().getTime() + 60 * 60000).toISOString(), + payin: { + currencyCode: 'WIRE_TRANSFER', + amount: rfq.data.payin.amount + }, + payout: { + currencyCode: 'STORED_BALANCE', + amount: rfq.data.payin.amount + } + } + }) + await quote.sign(config.pfiDid) + return quote + + } + + private async withdrawalQuote(rfq: Rfq) { + const quote = Quote.create({ + metadata: { + from: config.pfiDid.uri, + to: rfq.from, + exchangeId: rfq.exchangeId + }, + data: { + expiresAt: new Date(new Date().getTime() + 60 * 60000).toISOString(), + payin: { + currencyCode: 'STORED_BALANCE', + amount: rfq.data.payin.amount + }, + payout: { + currencyCode: 'WIRE_TRANSFER', + amount: rfq.data.payin.amount + } + } + }) + await quote.sign(config.pfiDid) + return quote + } + + private async BtcKesQuote(rfq: Rfq) { + const quote = Quote.create({ + metadata: { + from: config.pfiDid.uri, + to: rfq.from, + exchangeId: rfq.exchangeId + }, + data: { + expiresAt: new Date(new Date().getTime() + 60 * 60000).toISOString(), + payin: { + currencyCode: 'BTC', + amount: '1000.00' + }, + payout: { + currencyCode: 'KES', + amount: '123456789.00' + } + } + }) + await quote.sign(config.pfiDid) + return quote + } } const aliceMessageKinds = new Set(['rfq', 'order']) diff --git a/src/example/full-stored-balances.ts b/src/example/full-stored-balances.ts new file mode 100644 index 0000000..077ef28 --- /dev/null +++ b/src/example/full-stored-balances.ts @@ -0,0 +1,140 @@ +import { + TbdexHttpClient, + Rfq, + Quote, + Order, + OrderStatus, + Close, +} from '@tbdex/http-client' +import { createOrLoadDid } from './utils.js' +import { BearerDid } from '@web5/dids' +import fs from 'fs' + + +// load pfiDid from pfiDid.txt +const pfiDid = fs.readFileSync('pfiDid.txt', 'utf-8').trim() + + +const signedCredential = fs.readFileSync('signedCredential.txt', 'utf-8').trim() + +// +// Connect to the PFI and get the list of offerings (offerings are resources - anyone can ask for them) +// +const offerings = await TbdexHttpClient.getOfferings({ pfiDid: pfiDid }) +console.log('got offerings:', JSON.stringify(offerings, null, 2)) + + +// +// Load alice's private key to sign RFQ +// +const alice = await createOrLoadDid('alice.json') + +const [balances] = await TbdexHttpClient.getBalances({ pfiDid: pfiDid, did: alice }) +console.log('got balances:', JSON.stringify(balances, null, 2)) + +// lets make a deposit +let offering = offerings[1] +console.log('deposit offering', offering) +const rfq = Rfq.create({ + metadata: { from: alice.uri, to: pfiDid }, + data: { + offeringId: offerings[1].id, + payin: { + kind: 'WIRE_TRANSFER', + amount: '100.00', + paymentDetails: {}, + }, + payout: { + kind: 'STORED_BALANCE', + paymentDetails: {}, + }, + claims: [], // no claims for now - will add in kcc soon + }, +}) + +await rfq.sign(alice) + +try { + await TbdexHttpClient.createExchange(rfq) +} catch (error) { + console.log('Can\'t create:', error) +} +console.log('sent RFQ: ', JSON.stringify(rfq, null, 2)) + +let quote + +//Wait for Quote message to appear in the exchange +while (!quote) { + const exchange = await TbdexHttpClient.getExchange({ + pfiDid: pfiDid, + did: alice, + exchangeId: rfq.exchangeId + }) + + quote = exchange.find(msg => msg instanceof Quote) + + if (!quote) { + // Wait 2 seconds before making another request + await new Promise(resolve => setTimeout(resolve, 2000)) + } +} + +// +// +// All interaction with the PFI happens in the context of an exchange. +// This is where for example a quote would show up in result to an RFQ: +const exchange = await TbdexHttpClient.getExchange({ + pfiDid: pfiDid, + did: alice, + exchangeId: rfq.exchangeId +}) + +console.log('got exchange:', JSON.stringify(exchange, null, 2)) + +// Place an order against that quote: +const order = Order.create({ + metadata: { + from: alice.uri, + to: pfiDid, + exchangeId: quote.exchangeId + }, +}) +await order.sign(alice) +await TbdexHttpClient.submitOrder(order) +console.log('Sent order: ', JSON.stringify(order, null, 2)) + +await pollForStatus(order, pfiDid, alice) + + + +/* + * This is a very simple polling function that will poll for the status of an order. + */ +async function pollForStatus(order: Order, pfiDid: string, did: BearerDid) { + let close: Close + while (!close) { + const exchange = await TbdexHttpClient.getExchange({ + pfiDid: pfiDid, + did: did, + exchangeId: order.exchangeId + }) + + for (const message of exchange) { + if (message instanceof OrderStatus) { + console.log('we got a new order status') + const orderStatus = message as OrderStatus + console.log('orderStatus', JSON.stringify(orderStatus, null, 2)) + } else if (message instanceof Close) { + console.log('we have a close message') + close = message as Close + console.log('close', JSON.stringify(close, null, 2)) + return close + } + } + } +} + +const [end_balances] = await TbdexHttpClient.getBalances({ pfiDid: pfiDid, did: alice }) +console.log('starting balances:', JSON.stringify(balances, null, 2)) +console.log('end balances:', JSON.stringify(end_balances, null, 2)) +