Skip to content

Commit

Permalink
replyTo DID (#1414)
Browse files Browse the repository at this point in the history
* replyTo DID

* enabling test

* publishing pfi did
  • Loading branch information
angiejones committed May 5, 2024
1 parent 1548195 commit f3e4448
Show file tree
Hide file tree
Showing 6 changed files with 199 additions and 18 deletions.
3 changes: 2 additions & 1 deletion site/docs/tbdex/pfi/creating-quotes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ With the `Quote` created, you’ll then sign it for authorization purposes and w
]} />

:::tip Note
If the Wallet Application supplied a `replyTo` address with their RFQ, you'll send the Quote to that address.
If the Wallet Application supplied a `replyTo` address with their RFQ, the tbDEX SDK will send the Quote to that address.

If not, the Wallet Application will poll your PFI awaiting the Quote message to appear within the exchange.
:::
3 changes: 2 additions & 1 deletion site/docs/tbdex/pfi/processing-orders.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ As you deem appropriate for your customers, you can provide them with <LanguageS
{ snippetName: 'pfiOrderStatusKt', language: 'Kotlin'}
]} />

If the Wallet application supplied a `replyTo` address, you'll send the `OrderStatus` message there.
If the Wallet application supplied a `replyTo` address, the tbDEX SDK will send the `OrderStatus` message there.

You should also write the `OrderStatus` to your database and the Wallet will poll for these updates.


Expand Down
66 changes: 58 additions & 8 deletions site/docs/tbdex/wallet/send-rfq.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ Before sending the RFQ to the PFI, you can verify that the `data` within the RFQ
{ snippetName: 'verifyOfferingRequirementsJS', language: 'JavaScript' },
{ snippetName: 'verifyOfferingRequirementsKt', language: 'Kotlin' }
]}
inlineSnippets={[
inlineSnippets={[
{
code: '// verifyOfferings() is not available in the Swift SDK',
language: 'Swift',
Expand Down Expand Up @@ -101,7 +101,8 @@ Signing the RFQ ensures its authenticity. You can do so with the customer's [Bea


## Send RFQ to PFI
Use `TbdexHttpClient` to send the RFQ to the PFI, with an optional `replyTo` property containing a valid URI where new messages from the PFI will be sent:
Use `TbdexHttpClient` to send the RFQ to the PFI. If no error is thrown, the RFQ has been successfully sent to the PFI and an **exchange** has been created.
This exchange serves as a thread of messages between your application and the PFI regarding this transaction.

<Shnip
snippets={[
Expand All @@ -111,16 +112,65 @@ Use `TbdexHttpClient` to send the RFQ to the PFI, with an optional `replyTo` pro
]}
/>

:::tip Callback
Callbacks are fully qualified URIs (DID or URL) that can be provided to the PFI via the `replyTo` property.
The next message written to the exchange will be a [Quote](/docs/tbdex/wallet/receive-quote) from the PFI.
You'll need to [poll the PFI](/docs/tbdex/wallet/receive-quote#polling-for-quote) to receive the `Quote` and all other messages within the exchange, unless you provide an `replyTo` callback when sending the RFQ.

### Optional Callback
If your wallet application has a deployed server, you can provide a callback when sending the RFQ.
Callbacks are fully qualified URIs (DID or URL) that can be provided to the PFI via the optional `replyTo` property.

If `replyTo` is provided, the PFI will send all new messages of the exchange to the supplied URI.
This URI is scoped to each exchange, allowing you to specify a different URI per exchange if desired.

If `replyTo` is _not_ present, you will need to poll the PFI to receive new messages within the exchange.
:::tip No Callback
If a callback is not provided, you'll need to [poll the PFI](/docs/tbdex/wallet/receive-quote#polling-for-quote) to receive the `Quote` and all other messages within the exchange.
:::

If no error is thrown, the RFQ has been successfully sent to the PFI and an **exchange** has been created.
This exchange serves as a thread of messages between your application and the PFI regarding this transaction.
#### Callback as a URL

<Shnip
snippets={[
{ snippetName: 'rfqWithUrlReplyToJS', language: 'JavaScript' },
{ snippetName: 'rfqWithUrlReplyToKt', language: 'Kotlin' }
]}
inlineSnippets={[
{
code: '// replyTo is not available in the Swift SDK',
language: 'Swift',
},
]}
/>

#### Callback as a DID

To provide your wallet application's DID as the `replyTo` property, it must have a service type of `tbdex`.

<Shnip
snippets={[
{ snippetName: 'createDidWithTbdexServiceJS', language: 'JavaScript' },
{ snippetName: 'createDidWithTbdexServiceKt', language: 'Kotlin' }
]}
inlineSnippets={[
{
code: '// replyTo is not available in the Swift SDK',
language: 'Swift',
},
]}
/>

To use your DID as the `replyTo`, you'll provide its `uri`.

<Shnip
snippets={[
{ snippetName: 'rfqWithDidReplyToJS', language: 'JavaScript' },
{ snippetName: 'rfqWithDidReplyToKt', language: 'Kotlin' }
]}
inlineSnippets={[
{
code: '// replyTo is not available in the Swift SDK',
language: 'Swift',
},
]}
/>


The next message written to the exchange will be a [Quote](/docs/tbdex/wallet/receive-quote) from the PFI.
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { test, expect, describe, beforeAll, afterAll } from 'vitest';
import { TbdexHttpClient, DevTools, Rfq } from '@tbdex/http-client';
import { TbdexHttpClient, DevTools, Rfq, Offering } from '@tbdex/http-client';
import { DidDht } from '@web5/dids';
import { setupServer } from 'msw/node'
import { http, HttpResponse } from 'msw'

let pfiDid;
let customerDid;
let server;
let rfq;
let offering;
let selectedOffering;

describe('Wallet: Send RFQ', () => {
Expand All @@ -18,6 +20,7 @@ describe('Wallet: Send RFQ', () => {

pfiDid = await DidDht.create({
options:{
publish: true,
services : [{
type : 'PFI',
id : 'pfi',
Expand All @@ -26,11 +29,40 @@ describe('Wallet: Send RFQ', () => {
}
})

// Offering with claims (non-replyTo test)
selectedOffering = DevTools.createOffering({
from: pfiDid.uri
});
await selectedOffering.sign(pfiDid)

// Offering and RFQ (without claims) for replyTo tests
offering = Offering.create({
metadata: { from: pfiDid.uri },
data: {
id: '1234',
description: 'A test offering',
payin: {currencyCode: 'USD', methods: [{kind: 'DEBIT_CARD'}]},
payout: {currencyCode: 'BTC', methods: [{kind: 'BTC_ADDRESS', estimatedSettlementTime: 60}]},
payoutUnitsPerPayinUnit: '0.0001'
}
});
await offering.sign(pfiDid)

rfq = Rfq.create({
metadata: {
to: pfiDid.uri,
from: customerDid.uri,
protocol: '1.0'
},
data: {
offeringId: offering.metadata.id,
payin: {amount: '500.65', kind: 'DEBIT_CARD'},
payout: {kind: 'BTC_ADDRESS'},
}
})
await rfq.sign(customerDid)


// Mock the response from the PFI
server = setupServer(
http.post(new RegExp('https://localhost:9000/exchanges'), () => {
Expand Down Expand Up @@ -82,10 +114,9 @@ describe('Wallet: Send RFQ', () => {
});

test('create signed RFQ message and send to PFI', async () => {

const BTC_ADDRESS = 'bc1q52csjdqa6cq5d2ntkkyz8wk7qh2qevy04dyyfd'
const yoloCredential = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKamNuWWlPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aWVDSTZJalJ2WTE5eGRuVkZPVzEyUldkNFpXRmZlbVJYY1MxUlZVUlJRemswZWpGTlZVbFhaa1F6V1V4b2JVa2lMQ0pyYVdRaU9pSmtZazF3V25OT1ZHcE9ZbmQ2WW5OMFZXOVVTbU5aZFRKS1RIQkNhR2xCUnpRd1JYcDRORXRHVWsxbklpd2lZV3huSWpvaVJXUkVVMEVpZlEjMCJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiWW9sb0NyZWRlbnRpYWwiXSwiaWQiOiJ1cm46dXVpZDo4YjBmNjA3Zi1mMTdlLTRjNDktODczNS02YzU2MmU2N2U1NDEiLCJpc3N1ZXIiOiJkaWQ6andrOmV5SmpjbllpT2lKRlpESTFOVEU1SWl3aWEzUjVJam9pVDB0UUlpd2llQ0k2SWpSdlkxOXhkblZGT1cxMlJXZDRaV0ZmZW1SWGNTMVJWVVJSUXprMGVqRk5WVWxYWmtReldVeG9iVWtpTENKcmFXUWlPaUprWWsxd1duTk9WR3BPWW5kNlluTjBWVzlVU21OWmRUSktUSEJDYUdsQlJ6UXdSWHA0TkV0R1VrMW5JaXdpWVd4bklqb2lSV1JFVTBFaWZRIiwiaXNzdWFuY2VEYXRlIjoiMjAyNC0wNS0wMlQwNDoyNTo0NFoiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDpqd2s6ZXlKamNuWWlPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aWVDSTZJalJ2WTE5eGRuVkZPVzEyUldkNFpXRmZlbVJYY1MxUlZVUlJRemswZWpGTlZVbFhaa1F6V1V4b2JVa2lMQ0pyYVdRaU9pSmtZazF3V25OT1ZHcE9ZbmQ2WW5OMFZXOVVTbU5aZFRKS1RIQkNhR2xCUnpRd1JYcDRORXRHVWsxbklpd2lZV3huSWpvaVJXUkVVMEVpZlEiLCJiZWVwIjoiYm9vcCJ9fSwibmJmIjoxNzE0NjIzOTQ0LCJqdGkiOiJ1cm46dXVpZDo4YjBmNjA3Zi1mMTdlLTRjNDktODczNS02YzU2MmU2N2U1NDEiLCJpc3MiOiJkaWQ6andrOmV5SmpjbllpT2lKRlpESTFOVEU1SWl3aWEzUjVJam9pVDB0UUlpd2llQ0k2SWpSdlkxOXhkblZGT1cxMlJXZDRaV0ZmZW1SWGNTMVJWVVJSUXprMGVqRk5WVWxYWmtReldVeG9iVWtpTENKcmFXUWlPaUprWWsxd1duTk9WR3BPWW5kNlluTjBWVzlVU21OWmRUSktUSEJDYUdsQlJ6UXdSWHA0TkV0R1VrMW5JaXdpWVd4bklqb2lSV1JFVTBFaWZRIiwic3ViIjoiZGlkOmp3azpleUpqY25ZaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpZUNJNklqUnZZMTl4ZG5WRk9XMTJSV2Q0WldGZmVtUlhjUzFSVlVSUlF6azBlakZOVlVsWFprUXpXVXhvYlVraUxDSnJhV1FpT2lKa1lrMXdXbk5PVkdwT1luZDZZbk4wVlc5VVNtTlpkVEpLVEhCQ2FHbEJSelF3UlhwNE5FdEdVazFuSWl3aVlXeG5Jam9pUldSRVUwRWlmUSIsImlhdCI6MTcxNDYyMzk0NH0.CMZVBfNCq5aYgWmRcJVFN5fXiuPlgrwiGAsmYOZsFLHaRfqiA5gxqPDjBAQ1Ra7gK5X6_tZm5ue6kU6hN_7ZAA"
const selectedCredentials = [yoloCredential]
const selectedCredentials = [yoloCredential]

// :snippet-start: createRfqMessageJS
const rfq = Rfq.create({
Expand Down Expand Up @@ -135,15 +166,59 @@ describe('Wallet: Send RFQ', () => {

try{
// :snippet-start: sendRfqMessageJS
await TbdexHttpClient.createExchange(rfq);
// :snippet-end:
}
catch (e) {
expect.fail(`Failed to send RFQ message to PFI: ${e.message}`)
}
expect(rfq.signature).toBeDefined();
});

test('send RFQ message with URL as replyTo', async () => {
try{
// :snippet-start: rfqWithUrlReplyToJS
await TbdexHttpClient.createExchange(
rfq,
//highlight-next-line
{ replyTo: 'https://example.com/callback' }
);
// :snippet-end:
}
catch (e) {
expect.fail(`Failed to send RFQ message to PFI: ${e.message}`)
expect.fail(`Failed to send RFQ with URL replyTo: ${e.message}`)
}
});

test('send RFQ message with DID as replyTo', async () => {
// :snippet-start: createDidWithTbdexServiceJS
const walletDid = await DidDht.create({
options: {
publish: true,
services: [
{
id: 'tbdex',
//highlight-start
type: 'tbdex',
serviceEndpoint: 'https://example.com/callback'
//highlight-end
},
],
},
});
// :snippet-end:

try{
// :snippet-start: rfqWithDidReplyToJS
await TbdexHttpClient.createExchange(
rfq,
//highlight-next-line
{ replyTo: walletDid.uri }
);
// :snippet-end:
}
catch (e) {
expect.fail(`Failed to send RFQ message with DID as replyTo: ${e.message}`)
}
expect(rfq.signature).toBeDefined();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ export class MockOfferingsApiProvider {
//---------------------------------------------------------------------------//

async getOffering(id) {
console.log('called getOffering', id)
this.dataProvider.get('offering', id).then(([result]) => {
return Offering.create({
metadata: { from: this.pfiDid },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import org.junit.jupiter.api.Test
import tbdex.sdk.httpclient.TbdexHttpClient
import tbdex.sdk.httpclient.models.TbdexResponseException
import tbdex.sdk.protocol.models.*
import web5.sdk.crypto.InMemoryKeyManager
import web5.sdk.dids.didcore.Service
import web5.sdk.dids.methods.dht.CreateDidDhtOptions
import web5.sdk.dids.methods.dht.DidDht
import java.net.HttpURLConnection
import website.tbd.developer.site.docs.utils.TestData

Expand All @@ -22,12 +26,16 @@ class SendRfqTest {
private val customerDid = TestData.ALICE_DID
private lateinit var selectedOffering: Offering
private lateinit var server: MockWebServer
private lateinit var rfq: Rfq

@BeforeEach
fun setup() {
selectedOffering = TestData.getOfferingWithNoClaims(pfi.uri)
selectedOffering.sign(pfi)

rfq = TestData.getRfq()
rfq.sign(customerDid)

//Mock PFI Server
server = MockWebServer()
server.start(9000) // pfiDid resolves to https://localhost:9000
Expand Down Expand Up @@ -119,15 +127,62 @@ class SendRfqTest {

try{
// :snippet-start: sendRfqMessageKt
TbdexHttpClient.createExchange(rfq)
// :snippet-end:

}catch(e: TbdexResponseException){
fail("Failed to send RFQ message to PFI: $e")
}
assertNotNull(rfq.signature, "RFQ is not signed")
}

@Test
fun `send RFQ message with URL as replyTo`(){
try {
// :snippet-start: rfqWithUrlReplyToKt
TbdexHttpClient.createExchange(
rfq,
//highlight-next-line
"https://example.com/callback"
)
// :snippet-end:
}catch(e: TbdexResponseException){
fail("Failed to send RFQ with URL replyTo: $e")
}
}


@Test
fun `send RFQ message with DID as replyTo`(){
val keyManager = InMemoryKeyManager()

// :snippet-start: createDidWithTbdexServiceKt
val serviceToAdd = Service.Builder()
.id("tbdex")
//highlight-start
.type("tbdex")
.serviceEndpoint(listOf("https://example.com/callback"))
//highlight-end
.build()

val options = CreateDidDhtOptions(
publish = true,
services = listOf(serviceToAdd),
)

val walletDid = DidDht.create(keyManager, options)
// :snippet-end:

try {
// :snippet-start: rfqWithDidReplyToKt
TbdexHttpClient.createExchange(
rfq,
//highlight-next-line
walletDid.uri
)
// :snippet-end:
}catch(e: TbdexResponseException){
fail("Failed to send RFQ message to PFI: $e")
fail("Failed to send RFQ with DID replyTo: $e")
}
assertNotNull(rfq.signature, "RFQ is not signed")
}
}

0 comments on commit f3e4448

Please sign in to comment.