Skip to content

Commit

Permalink
Added verifyOfferingRequirements (#1410)
Browse files Browse the repository at this point in the history
  • Loading branch information
angiejones committed May 2, 2024
1 parent 4ca7538 commit b893ed6
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 96 deletions.
30 changes: 30 additions & 0 deletions site/docs/tbdex/wallet/send-rfq.mdx
Expand Up @@ -57,6 +57,36 @@ The data properties contain the specifics of the transaction, such as the id of
]}
/>

## Verify RFQ Data
Before sending the RFQ to the PFI, you can verify that the `data` within the RFQ satifies the Offering's requirements.

<Shnip
snippets={[
{ snippetName: 'verifyOfferingRequirementsJS', language: 'JavaScript' },
{ snippetName: 'verifyOfferingRequirementsKt', language: 'Kotlin' }
]}
inlineSnippets={[
{
code: '// verifyOfferings() is not available in the Swift SDK',
language: 'Swift',
},
]}
/>

This will throw an error for any of the following:
* `data.offeringId` doesn't match the provided Offering's id
* `data.payin.amount` exceeds the provided Offering's max units allowed or is below the Offering's min units allowed
* `data.payin.kind` does not match the provided Offering's payin kinds
* `data.payout.kind` does not match the provided Offering's payout kinds
* `data.payin.paymentDetails` doesn't satisfy the Offering's `requiredPaymentDetails`
* `data.payout.paymentDetails` doesn't satisfy the Offering's `requiredPaymentDetails`
* `data.claims` does not contain JWTs for VCs that match the Offering's `requiredClaims`

**Example error output**

> Error: offering does not support rfq's payinMethod kind. (rfq) BTC_ADDRESS was not found in: [BANK_ACCOUNT] (offering)


## Sign the RFQ
Signing the RFQ ensures its authenticity. You can do so with the customer's [Bearer DID](/docs/web5/build/decentralized-identifiers/how-to-create-did#create-did):
Expand Down
Expand Up @@ -84,7 +84,8 @@ describe('Wallet: Send RFQ', () => {
test('create signed RFQ message and send to PFI', async () => {

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

// :snippet-start: createRfqMessageJS
const rfq = Rfq.create({
Expand All @@ -97,19 +98,19 @@ describe('Wallet: Send RFQ', () => {
data: {
offeringId: selectedOffering.metadata.id, // The ID of the selected offering
payin: {
kind: 'BTC_ADDRESS', // The method of payment
amount: '0.012', // The amount of the payin currency
kind: 'DEBIT_CARD', // The method of payment
amount: '500.65', // The amount of the payin currency
paymentDetails: {
btcAddress: BTC_ADDRESS // Customer's BTC wallet address
cardNumber: '1234567890123456',
expiryDate: '05/25',
cardHolderName: 'Alice Doe',
cvv: '123'
}
},
payout: {
kind: 'DEBIT_CARD', // The method for receiving payout
kind: 'BTC_ADDRESS', // The method for receiving payout
paymentDetails: {
cvv: '123',
cardNumber: '1234567890123456789',
expiryDate: '05/25',
cardHolderName: 'Alice Doe'
btcAddress: BTC_ADDRESS // Recipient's BTC wallet address
}
},
claims: selectedCredentials // Array of signed VCs required by the PFI
Expand All @@ -118,6 +119,16 @@ describe('Wallet: Send RFQ', () => {
});
// :snippet-end:

expect(() => {
// :snippet-start: verifyOfferingRequirementsJS
try{
rfq.verifyOfferingRequirements(selectedOffering);
} catch (e) {
// handle failed verification
}
// :snippet-end:
}).not.toThrow();

// :snippet-start: signRfqMessageJS
await rfq.sign(customerDid);
// :snippet-end:
Expand All @@ -135,4 +146,4 @@ describe('Wallet: Send RFQ', () => {
}
expect(rfq.signature).toBeDefined();
});
});
});
Expand Up @@ -18,33 +18,30 @@ import website.tbd.developer.site.docs.utils.TestData
*/
class SendRfqTest {

private val pfi = TestData.PFI_DID
private val customerDid = TestData.ALICE_DID
private lateinit var selectedOffering: Offering
private lateinit var server: MockWebServer

@BeforeEach
fun setup() {
selectedOffering = TestData.getOffering(
pfi.uri,
TestData.getPresentationDefinition()
)
selectedOffering.sign(pfi)
private val pfi = TestData.PFI_DID
private val customerDid = TestData.ALICE_DID
private lateinit var selectedOffering: Offering
private lateinit var server: MockWebServer

//Mock PFI Server
server = MockWebServer()
server.start(9000) // pfiDid resolves to https://localhost:9000
server.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK))
}
@BeforeEach
fun setup() {
selectedOffering = TestData.getOfferingWithNoClaims(pfi.uri)
selectedOffering.sign(pfi)

@AfterEach
fun tearDown() {
server.shutdown()
}
//Mock PFI Server
server = MockWebServer()
server.start(9000) // pfiDid resolves to https://localhost:9000
server.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK))
}

@AfterEach
fun tearDown() {
server.shutdown()
}

@Test
fun `get all skeleton RFQ - properties`() {
val skeleton = """
@Test
fun `get all skeleton RFQ - properties`() {
val skeleton = """
// :snippet-start: skeletonRfqMessageKt
val rfq = Rfq.create(
to, //metadata
Expand All @@ -54,12 +51,12 @@ class SendRfqTest {
// :snippet-end:
"""

//no assertions needed; this is just showing how to structure a RFQ
}
//no assertions needed; this is just showing how to structure a RFQ
}

@Test
fun `skeleton RFQ - metadata`() {
val skeleton = """
@Test
fun `skeleton RFQ - metadata`() {
val skeleton = """
// :snippet-start: rfqMetadataKt
val rfq = Rfq.create(
//metadata
Expand All @@ -73,56 +70,64 @@ class SendRfqTest {
// :snippet-end:
"""

//no assertions needed; this is just showing how to structure a RFQ
}
//no assertions needed; this is just showing how to structure a RFQ
}

@Test
fun `create signed RFQ message and send to PFI`() {
val BTC_ADDRESS = "bc1q52csjdqa6cq5d2ntkkyz8wk7qh2qevy04dyyfd"
val selectedCredentials = emptyList<String>()
@Test
fun `create signed RFQ message and send to PFI`() {
val BTC_ADDRESS = "bc1q52csjdqa6cq5d2ntkkyz8wk7qh2qevy04dyyfd"
val selectedCredentials = emptyList<String>()

// :snippet-start: createRfqMessageKt
val rfq = Rfq.create(
to = selectedOffering.metadata.from, // PFI's DID
from = customerDid.uri, // Customer DID
//highlight-start
rfqData = CreateRfqData(
offeringId = selectedOffering.metadata.id, // The ID of the selected offering
payin = CreateSelectedPayinMethod(
kind = "BTC_ADDRESS", // The method of payment
paymentDetails = mapOf("btcAddress" to BTC_ADDRESS), // Customer's BTC wallet address
amount = "0.012", // The amount of the payin currency
),
payout = CreateSelectedPayoutMethod(
kind = "DEBIT_CARD", // The method for receiving payout
paymentDetails = mapOf(
"cvv" to "123",
"cardNumber" to "1234567890123456789",
"expiryDate" to "05/25",
"cardHolderName" to "Alice Doe"
)
),
claims = selectedCredentials // Array of signed VCs required by the PFI
)
//highlight-end
)
// :snippet-end:
// :snippet-start: createRfqMessageKt
val rfq = Rfq.create(
to = selectedOffering.metadata.from, // PFI's DID
from = customerDid.uri, // Customer DID
//highlight-start
rfqData = CreateRfqData(
offeringId = selectedOffering.metadata.id, // The ID of the selected offering
payin = CreateSelectedPayinMethod(
kind = "DEBIT_CARD", // The method of payment
paymentDetails = mapOf(
"cvv" to "123",
"cardNumber" to "1234567890123456",
"expiryDate" to "05/25",
"cardHolderName" to "Alice Doe"
),
amount = "500.65", // The amount of the payin currency
),
payout = CreateSelectedPayoutMethod(
kind = "BTC_ADDRESS", // The method for receiving payout
paymentDetails = mapOf("btcAddress" to BTC_ADDRESS), // Recipient's BTC wallet address
),
claims = selectedCredentials // List of signed VCs required by the PFI
)
//highlight-end
)
// :snippet-end:

// :snippet-start: signRfqMessageKt
rfq.sign(customerDid);
// :snippet-end:
// :snippet-start: verifyOfferingRequirementsKt
try{
rfq.verifyOfferingRequirements(selectedOffering)
}catch (e: Exception){
// handle failed verification
}
// :snippet-end:

// :snippet-start: signRfqMessageKt
rfq.sign(customerDid);
// :snippet-end:

try{
// :snippet-start: sendRfqMessageKt
TbdexHttpClient.createExchange(
rfq,
"https://example.com/callback"
)
// :snippet-end:

try{
// :snippet-start: sendRfqMessageKt
TbdexHttpClient.createExchange(
rfq,
"https://example.com/callback"
)
// :snippet-end:

}catch(e: TbdexResponseException){
fail("Failed to send RFQ message to PFI: $e")
}
assertNotNull(rfq.signature, "RFQ is not signed")
}catch(e: TbdexResponseException){
fail("Failed to send RFQ message to PFI: $e")
}
assertNotNull(rfq.signature, "RFQ is not signed")
}
}
Expand Up @@ -17,6 +17,7 @@ import web5.sdk.dids.methods.dht.CreateDidDhtOptions
import web5.sdk.dids.methods.dht.DidDht
import java.net.URI
import java.time.OffsetDateTime
import java.time.OffsetDateTime.*
import java.util.*

object TestData {
Expand Down Expand Up @@ -87,7 +88,32 @@ object TestData {
)
)

fun getRfq(
fun getOfferingWithNoClaims(
from: String = PFI_DID.uri
) = Offering.create(
from = from,
OfferingData(
description = "A sample offering",
payoutUnitsPerPayinUnit = "1",
payin = PayinDetails("USD", "0.01", "100.00", listOf(PayinMethod(
kind = "DEBIT_CARD",
requiredPaymentDetails = requiredPaymentDetailsSchema()
))),
payout = PayoutDetails(
currencyCode = "BTC",
methods = listOf(PayoutMethod(
kind = "BTC_ADDRESS",
requiredPaymentDetails = requiredPaymentDetailsSchema(),
estimatedSettlementTime = 3600
)
)
),
requiredClaims = null
)
)


fun getRfq(
to: String = PFI_DID.uri,
from: String = ALICE_DID.uri,
offeringId: TypeId = TypeId.generate(ResourceKind.offering.name),
Expand Down Expand Up @@ -121,7 +147,7 @@ object TestData {
from: String = PFI_DID.uri) = Quote.create(
ALICE_DID.uri, PFI_DID.uri, TypeId.generate(MessageKind.rfq.name).toString(),
QuoteData(
expiresAt = OffsetDateTime.now().plusDays(1),
expiresAt = now().plusDays(1),
payin = QuoteDetails("USD", "10.00", "0.01"),
payout = QuoteDetails("KES", "0.12", "0.02"),
)
Expand Down
Expand Up @@ -59,19 +59,20 @@ final class SendingRfqTests: XCTestCase {
data: CreateRFQData(
offeringId: selectedOffering.metadata.id, // The ID of the selected offering
payin: CreateRFQPayinMethod(
amount: "0.012", // The amount of the payin currency
kind: "BTC_ADDRESS", // The method for sending payment
paymentDetails: [
"btc_address": BTC_ADDRESS // Customer's BTC wallet address
]),
payout: CreateRFQPayoutMethod(
kind: "DEBIT_CARD", // The method for receiving payout
amount: "500.65", // The amount of the payin currency
kind: "DEBIT_CARD", // The method for sending payment
paymentDetails: [
"cvv": "123",
"cardNumber": "1234567890123456789",
"cardNumber": "1234567890123456",
"expiryDate": "05/25",
"cardHolderName": "Alice Doe"
]
),
payout: CreateRFQPayoutMethod(
kind: "BTC_ADDRESS", // The method for receiving payout
paymentDetails: [
"btc_address": BTC_ADDRESS // Recipient's BTC wallet address
]
),
claims: selectedCredentials
),
Expand Down

0 comments on commit b893ed6

Please sign in to comment.