@taprsvp/types
Version:
TypeScript types and interfaces for the Transaction Authorization Protocol (TAP)
615 lines (559 loc) • 22.2 kB
text/typescript
/**
* @fileoverview Fast-Check Arbitraries for TAP Message Types
*
* This module provides fast-check arbitraries that can generate valid TAP data structures
* for property-based testing.
*/
import * as fc from "fast-check";
import type {
DID,
IRI,
CAIP10,
CAIP19,
Amount,
Participant,
Person,
Organization,
Agent,
Transfer,
Payment,
RFQ,
Quote,
Lock,
Capture,
Authorize,
Connect,
Settle,
Reject,
Cancel,
Revert,
TransferMessage,
PaymentMessage,
RFQMessage,
QuoteMessage,
LockMessage,
CaptureMessage,
AuthorizeMessage,
ConnectMessage,
SettleMessage,
RejectMessage,
CancelMessage,
RevertMessage,
TAPMessage,
TransactionConstraints,
} from "./tap";
import type { IsoCurrency } from "./currencies";
import { arbitraries as ivms101 } from 'ivms101';
// ============================================================================
// FUNDAMENTAL TYPE ARBITRARIES
// ============================================================================
export const did = (): fc.Arbitrary<DID> =>
fc.oneof(
// did:web with valid domain names
fc.domain().map(domain => `did:web:${domain}` as DID),
// did:ethr with Ethereum addresses (simplified format)
fc.string({ minLength: 40, maxLength: 40, unit: fc.constantFrom('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f') })
.map(ethAddress => `did:ethr:0x${ethAddress}` as DID),
// did:key with standard multibase key
fc.constantFrom(
"did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp",
"did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
"did:key:z6Mkfriq1MqLBoPWecGoDHRzEyqZY6ksafzCJ8nfaqJNgHgN"
),
// Generic did:example for testing
fc.constantFrom(
"did:example:123456789abcdefg",
"did:example:alice",
"did:example:bob"
),
// did:pkh with simplified identifier (validator doesn't support full CAIP-10)
fc.string({ minLength: 20, maxLength: 50, unit: fc.constantFrom(...'abcdefghijklmnopqrstuvwxyz0123456789._%-'.split('')) })
.map(identifier => `did:pkh:${identifier}` as DID)
);
export const iri = (): fc.Arbitrary<IRI> =>
fc.oneof(
fc.webUrl() as fc.Arbitrary<IRI>,
fc.record({
scheme: fc.constantFrom("mailto", "urn", "tel"),
path: fc.string({ minLength: 5, maxLength: 50 })
}).map(({ scheme, path }) => `${scheme}:${path}` as IRI)
);
export const uuid = (): fc.Arbitrary<string> =>
fc.uuid();
export const caip10 = (): fc.Arbitrary<CAIP10> =>
fc.oneof(
// Ethereum addresses
fc.record({
namespace: fc.constant("eip155"),
reference: fc.constantFrom("1", "137", "56", "10", "42161"),
address: fc.string({ minLength: 40, maxLength: 40, unit: fc.constantFrom('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f') })
}).map(({ namespace, reference, address }) => `${namespace}:${reference}:0x${address}` as CAIP10),
// Bitcoin addresses
fc.record({
namespace: fc.constant("bip122"),
reference: fc.constantFrom("000000000019d6689c085ae165831e93", "000000000000000000651ef99cb9fcbe"),
address: fc.constantFrom("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4")
}).map(({ namespace, reference, address }) => `${namespace}:${reference}:${address}` as CAIP10),
// Cosmos addresses
fc.record({
namespace: fc.constant("cosmos"),
reference: fc.constantFrom("cosmoshub-3", "cosmoshub-4"),
address: fc.constantFrom("cosmos1t2uflqwqe0fsj0shcfkrvpukewcw40yjj6hdc0", "cosmos1fqzqejwkk898fcslw4z4eeqjzesynvwsjk3r8")
}).map(({ namespace, reference, address }) => `${namespace}:${reference}:${address}` as CAIP10)
);
export const caip19 = (): fc.Arbitrary<CAIP19> =>
fc.record({
namespace: fc.constantFrom("eip155", "bip122"),
reference: fc.string({ minLength: 1, maxLength: 32, unit: fc.constantFrom('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', '-') }),
assetName: fc.constantFrom("erc20", "slip44"),
assetId: fc.string({ minLength: 32, maxLength: 42, unit: fc.constantFrom('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f') })
}).map(({ namespace, reference, assetName, assetId }) =>
`${namespace}:${reference}/${assetName}:${assetId}` as CAIP19
);
export const amount = (): fc.Arbitrary<Amount> =>
fc.float({ min: Math.fround(0.01), max: Math.fround(1000000) })
.map(n => n.toFixed(2))
.filter(s => /^\d+\.\d{2}$/.test(s)) as fc.Arbitrary<Amount>;
export const isoCurrency = (): fc.Arbitrary<IsoCurrency> =>
fc.constantFrom("USD", "EUR", "GBP", "JPY", "CHF", "CAD", "AUD");
export const supportedAssetPricing = (): fc.Arbitrary<any> =>
fc.record({
asset: fc.oneof(caip19(), isoCurrency()),
amount: amount(),
expires: fc.option(
fc.constantFrom(
new Date(Date.now() + 3600000).toISOString(), // 1 hour from now
new Date(Date.now() + 86400000).toISOString(), // 1 day from now
new Date(Date.now() + 1800000).toISOString() // 30 minutes from now
),
{ nil: undefined }
)
});
// ============================================================================
// PARTICIPANT ARBITRARIES
// ============================================================================
export const participant = (): fc.Arbitrary<Participant> =>
fc.record({
"@id": did(),
name: fc.constantFrom("Alice Smith", "Bob Jones", "Carol Davis", "Dave Miller", "Eva Brown", "Frank Wilson", "Grace Taylor", "Henry Johnson"),
email: fc.option(fc.constantFrom("alice@example.com", "bob@test.org", "carol@demo.net"), { freq: 7 }),
telephone: fc.option(fc.constantFrom("+1-555-0123", "+44-20-7946-0958", "+49-30-12345678"), { freq: 5 })
}).map(obj => {
const result: any = { ...obj };
if (result.email === null) delete result.email;
if (result.telephone === null) delete result.telephone;
return result;
});
export const person = (): fc.Arbitrary<Person> =>
fc.record({
"@id": did(),
"@type": fc.constant("https://schema.org/Person" as const),
name: fc.constantFrom("Alice Smith", "Bob Jones", "Carol Davis", "Dave Miller", "Eva Brown"),
givenName: fc.option(fc.constantFrom("Alice", "Bob", "Carol", "Dave", "Eva"), { freq: 6 }),
familyName: fc.option(fc.constantFrom("Smith", "Jones", "Davis", "Miller", "Brown"), { freq: 6 }),
email: fc.option(fc.constantFrom("alice@example.com", "bob@test.org", "carol@demo.net"), { freq: 7 })
}).map(obj => {
const result: any = { ...obj };
if (result.givenName === null) delete result.givenName;
if (result.familyName === null) delete result.familyName;
if (result.email === null) delete result.email;
return result;
});
export const organization = (): fc.Arbitrary<Organization> =>
fc.record({
"@id": did(),
"@type": fc.constant("https://schema.org/Organization" as const),
name: fc.constantFrom("Acme Corp", "TechStart Inc", "Global Bank", "Crypto Exchange", "Payment Services"),
legalName: fc.option(fc.constantFrom("Acme Corporation Ltd", "TechStart Incorporated", "Global Banking Solutions"), { freq: 5 }),
url: fc.option(fc.constantFrom("https://acme.com", "https://techstart.io", "https://globalbank.net"), { freq: 6 })
}).map(obj => {
const result: any = { ...obj };
if (result.legalName === null) delete result.legalName;
if (result.url === null) delete result.url;
return result;
});
export const party = () => fc.oneof(person(), organization());
export const agent = (): fc.Arbitrary<Agent> =>
fc.record({
"@id": did(),
for: did(),
name: fc.constantFrom("Wallet Agent", "Exchange Agent", "Bank Agent", "Payment Agent", "Trading Bot"),
role: fc.option(fc.constantFrom("PaymentProcessor", "WalletProvider", "Exchange", "Bank"), { freq: 8 })
}).map(obj => {
const result: any = { ...obj };
if (result.role === null) delete result.role;
return result;
});
// ============================================================================
// MESSAGE BODY ARBITRARIES
// ============================================================================
export const transfer = (): fc.Arbitrary<Transfer> =>
fc.record({
"@context": fc.constant("https://tap.rsvp/schema/1.0" as const),
"@type": fc.constant("Transfer" as const),
asset: caip19(),
amount: amount(),
originator: party(),
beneficiary: party(),
agents: fc.array(agent(), { minLength: 1, maxLength: 3 }),
purposeCode: fc.option(fc.constantFrom("TRAD", "SALA", "RENT", "INTC", "SUPP"), { nil: undefined })
});
export const payment = (): fc.Arbitrary<Payment> =>
fc.record({
"@context": fc.constant("https://tap.rsvp/schema/1.0" as const),
"@type": fc.constant("Payment" as const),
amount: amount(),
currency: isoCurrency(),
supportedAssets: fc.option(
fc.array(
fc.oneof(
fc.oneof(caip19(), isoCurrency()), // Simple asset strings
supportedAssetPricing() // Pricing objects
),
{ minLength: 1, maxLength: 5 }
),
{ nil: undefined }
),
fallbackSettlementAddresses: fc.option(
fc.array(fc.oneof(
caip10(),
fc.constantFrom(
"payto://iban/DE75512108001245126199" as any,
"payto://iban/GB29NWBK60161331926819" as any
)
), { minLength: 1, maxLength: 3 }),
{ nil: undefined }
),
merchant: party(),
customer: fc.option(party(), { nil: undefined }),
agents: fc.array(agent(), { minLength: 1, maxLength: 3 }),
expiry: fc.option(
fc.constantFrom(
new Date(Date.now() + 3600000).toISOString(), // 1 hour from now
new Date(Date.now() + 86400000).toISOString(), // 1 day from now
new Date(Date.now() + 1800000).toISOString() // 30 minutes from now
),
{ nil: undefined }
),
purposeCode: fc.option(fc.constantFrom("TRAD", "SALA", "RENT", "INTC", "SUPP"), { nil: undefined })
});
export const rfq = (): fc.Arbitrary<RFQ> =>
fc.oneof(
fc.record({
"@context": fc.constant("https://tap.rsvp/schema/1.0" as const),
"@type": fc.constant("RFQ" as const),
fromAssets: fc.array(fc.oneof(caip19(), isoCurrency()), { minLength: 1, maxLength: 5 }),
toAssets: fc.array(fc.oneof(caip19(), isoCurrency()), { minLength: 1, maxLength: 5 }),
fromAmount: amount(),
requester: party(),
provider: fc.option(party(), { nil: undefined }),
agents: fc.array(agent(), { minLength: 1, maxLength: 3 })
}),
fc.record({
"@context": fc.constant("https://tap.rsvp/schema/1.0" as const),
"@type": fc.constant("RFQ" as const),
fromAssets: fc.array(fc.oneof(caip19(), isoCurrency()), { minLength: 1, maxLength: 5 }),
toAssets: fc.array(fc.oneof(caip19(), isoCurrency()), { minLength: 1, maxLength: 5 }),
toAmount: amount(),
requester: party(),
provider: fc.option(party(), { nil: undefined }),
agents: fc.array(agent(), { minLength: 1, maxLength: 3 })
})
);
export const quote = (): fc.Arbitrary<Quote> =>
fc.record({
"@context": fc.constant("https://tap.rsvp/schema/1.0" as const),
"@type": fc.constant("Quote" as const),
fromAsset: fc.oneof(caip19(), isoCurrency()),
toAsset: fc.oneof(caip19(), isoCurrency()),
fromAmount: amount(),
toAmount: amount(),
provider: party(),
agents: fc.array(agent(), { minLength: 1, maxLength: 3 }),
expiresAt: fc.constantFrom(
new Date(Date.now() + 3600000).toISOString(), // 1 hour from now
new Date(Date.now() + 86400000).toISOString(), // 1 day from now
new Date(Date.now() + 604800000).toISOString() // 1 week from now
)
});
export const lock = (): fc.Arbitrary<Lock> =>
fc.record({
"@context": fc.constant("https://tap.rsvp/schema/1.0" as const),
"@type": fc.constant("Lock" as const),
asset: caip19(),
amount: amount(),
originator: party(),
beneficiary: party(),
expiry: fc.constantFrom(
new Date(Date.now() + 3600000).toISOString(), // 1 hour from now
new Date(Date.now() + 86400000).toISOString(), // 1 day from now
new Date(Date.now() + 604800000).toISOString() // 1 week from now
),
agents: fc.array(agent(), { minLength: 1, maxLength: 3 }),
agreement: fc.option(fc.webUrl(), { nil: undefined })
});
export const capture = (): fc.Arbitrary<Capture> =>
fc.record({
"@context": fc.constant("https://tap.rsvp/schema/1.0" as const),
"@type": fc.constant("Capture" as const),
amount: fc.option(amount(), { nil: undefined }),
settlementAddress: fc.option(caip10(), { nil: undefined })
});
export const authorize = (): fc.Arbitrary<Authorize> =>
fc.record({
"@context": fc.constant("https://tap.rsvp/schema/1.0" as const),
"@type": fc.constant("Authorize" as const),
decision: fc.constantFrom("approve", "deny"),
reason: fc.option(fc.string({ minLength: 10, maxLength: 100 }), { nil: undefined })
});
export const connect = (): fc.Arbitrary<Connect> =>
fc.record({
"@context": fc.constant("https://tap.rsvp/schema/1.0" as const),
"@type": fc.constant("Connect" as const),
requester: party(),
principal: party(),
agents: fc.array(agent(), { minLength: 1, maxLength: 3 }),
constraints: fc.record({
purposes: fc.option(fc.array(fc.constantFrom("TRAD", "SALA", "RENT", "INTC", "SUPP"), { minLength: 1, maxLength: 5 }), { nil: undefined })
})
});
export const settle = (): fc.Arbitrary<Settle> =>
fc.record({
"@context": fc.constant("https://tap.rsvp/schema/1.0" as const),
"@type": fc.constant("Settle" as const),
txHash: fc.string({ minLength: 64, maxLength: 64, unit: fc.constantFrom('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f') }),
settlementAddress: caip10()
});
export const reject = (): fc.Arbitrary<Reject> =>
fc.record({
"@context": fc.constant("https://tap.rsvp/schema/1.0" as const),
"@type": fc.constant("Reject" as const),
reason: fc.string({ minLength: 10, maxLength: 100 })
});
export const cancel = (): fc.Arbitrary<Cancel> =>
fc.record({
"@context": fc.constant("https://tap.rsvp/schema/1.0" as const),
"@type": fc.constant("Cancel" as const),
by: did(),
reason: fc.option(fc.string({ minLength: 10, maxLength: 100 }), { nil: undefined })
});
export const revert = (): fc.Arbitrary<Revert> =>
fc.record({
"@context": fc.constant("https://tap.rsvp/schema/1.0" as const),
"@type": fc.constant("Revert" as const),
settlementAddress: caip10(),
reason: fc.string({ minLength: 10, maxLength: 100 })
});
// ============================================================================
// DIDCOMM MESSAGE ARBITRARIES
// ============================================================================
export const didcommMessage = <T>(bodyArb: fc.Arbitrary<T>) =>
fc.record({
id: uuid(),
type: fc.string({ minLength: 10, maxLength: 100 }),
from: did(),
to: fc.array(did(), { minLength: 1, maxLength: 3 }),
created_time: fc.integer({ min: Date.now() - 86400000, max: Date.now() }),
body: bodyArb,
thid: fc.option(uuid(), { nil: undefined })
});
export const didcommReply = <T>(bodyArb: fc.Arbitrary<T>) =>
fc.record({
id: uuid(),
type: fc.string({ minLength: 10, maxLength: 100 }),
from: did(),
to: fc.array(did(), { minLength: 1, maxLength: 3 }),
created_time: fc.integer({ min: Date.now() - 86400000, max: Date.now() }),
thid: uuid(),
body: bodyArb
});
export const transferMessage = (): fc.Arbitrary<TransferMessage> =>
fc.record({
id: uuid(),
type: fc.constant("https://tap.rsvp/schema/1.0#Transfer" as const),
from: did(),
to: fc.array(did(), { minLength: 1, maxLength: 1 }),
created_time: fc.integer({ min: Date.now() - 86400000, max: Date.now() }),
body: transfer(),
thid: fc.option(uuid(), { nil: undefined })
});
export const paymentMessage = (): fc.Arbitrary<PaymentMessage> =>
fc.record({
id: uuid(),
type: fc.constant("https://tap.rsvp/schema/1.0#Payment" as const),
from: did(),
to: fc.array(did(), { minLength: 1, maxLength: 1 }),
created_time: fc.integer({ min: Date.now() - 86400000, max: Date.now() }),
body: payment(),
thid: fc.option(uuid(), { nil: undefined })
});
export const rfqMessage = (): fc.Arbitrary<RFQMessage> =>
fc.record({
id: uuid(),
type: fc.constant("https://tap.rsvp/schema/1.0#RFQ" as const),
from: did(),
to: fc.array(did(), { minLength: 1, maxLength: 1 }),
created_time: fc.integer({ min: Date.now() - 86400000, max: Date.now() }),
body: rfq(),
thid: fc.option(uuid(), { nil: undefined })
});
export const quoteMessage = (): fc.Arbitrary<QuoteMessage> =>
fc.record({
id: uuid(),
type: fc.constant("https://tap.rsvp/schema/1.0#Quote" as const),
from: did(),
to: fc.array(did(), { minLength: 1, maxLength: 1 }),
created_time: fc.integer({ min: Date.now() - 86400000, max: Date.now() }),
thid: uuid(),
body: quote()
});
export const lockMessage = (): fc.Arbitrary<LockMessage> =>
fc.record({
id: uuid(),
type: fc.constant("https://tap.rsvp/schema/1.0#Lock" as const),
from: did(),
to: fc.array(did(), { minLength: 1, maxLength: 1 }),
created_time: fc.integer({ min: Date.now() - 86400000, max: Date.now() }),
body: lock(),
thid: fc.option(uuid(), { nil: undefined })
});
export const captureMessage = (): fc.Arbitrary<CaptureMessage> =>
fc.record({
id: uuid(),
type: fc.constant("https://tap.rsvp/schema/1.0#Capture" as const),
from: did(),
to: fc.array(did(), { minLength: 1, maxLength: 1 }),
created_time: fc.integer({ min: Date.now() - 86400000, max: Date.now() }),
thid: uuid(),
body: capture()
});
export const authorizeMessage = (): fc.Arbitrary<AuthorizeMessage> =>
fc.record({
id: uuid(),
type: fc.constant("https://tap.rsvp/schema/1.0#Authorize" as const),
from: did(),
to: fc.array(did(), { minLength: 1, maxLength: 1 }),
created_time: fc.integer({ min: Date.now() - 86400000, max: Date.now() }),
thid: uuid(),
body: authorize()
});
export const connectMessage = (): fc.Arbitrary<ConnectMessage> =>
fc.record({
id: uuid(),
type: fc.constant("https://tap.rsvp/schema/1.0#Connect" as const),
from: did(),
to: fc.array(did(), { minLength: 1, maxLength: 1 }),
created_time: fc.integer({ min: Date.now() - 86400000, max: Date.now() }),
body: connect(),
thid: fc.option(uuid(), { nil: undefined })
});
export const settleMessage = (): fc.Arbitrary<SettleMessage> =>
fc.record({
id: uuid(),
type: fc.constant("https://tap.rsvp/schema/1.0#Settle" as const),
from: did(),
to: fc.array(did(), { minLength: 1, maxLength: 1 }),
created_time: fc.integer({ min: Date.now() - 86400000, max: Date.now() }),
thid: uuid(),
body: settle()
});
export const rejectMessage = (): fc.Arbitrary<RejectMessage> =>
fc.record({
id: uuid(),
type: fc.constant("https://tap.rsvp/schema/1.0#Reject" as const),
from: did(),
to: fc.array(did(), { minLength: 1, maxLength: 1 }),
created_time: fc.integer({ min: Date.now() - 86400000, max: Date.now() }),
thid: uuid(),
body: reject()
});
export const cancelMessage = (): fc.Arbitrary<CancelMessage> =>
fc.record({
id: uuid(),
type: fc.constant("https://tap.rsvp/schema/1.0#Cancel" as const),
from: did(),
to: fc.array(did(), { minLength: 1, maxLength: 1 }),
created_time: fc.integer({ min: Date.now() - 86400000, max: Date.now() }),
thid: uuid(),
body: cancel()
});
export const revertMessage = (): fc.Arbitrary<RevertMessage> =>
fc.record({
id: uuid(),
type: fc.constant("https://tap.rsvp/schema/1.0#Revert" as const),
from: did(),
to: fc.array(did(), { minLength: 1, maxLength: 1 }),
created_time: fc.integer({ min: Date.now() - 86400000, max: Date.now() }),
thid: uuid(),
body: revert()
});
export const tapMessage = (): fc.Arbitrary<TAPMessage> =>
fc.oneof(
transferMessage(),
paymentMessage(),
rfqMessage(),
quoteMessage(),
lockMessage(),
captureMessage(),
authorizeMessage(),
connectMessage(),
settleMessage(),
rejectMessage(),
cancelMessage(),
revertMessage()
);
// ============================================================================
// GROUPED EXPORT
// ============================================================================
export const arbitraries = {
// Fundamental types
fundamental: {
did: () => did(),
iri: () => iri(),
uuid: () => uuid(),
caip10: () => caip10(),
caip19: () => caip19(),
amount: () => amount(),
isoCurrency: () => isoCurrency(),
supportedAssetPricing: () => supportedAssetPricing(),
},
// Participants
participants: {
participant: () => participant(),
person: () => person(),
organization: () => organization(),
party: () => party(),
agent: () => agent(),
},
// Message bodies
messageBodies: {
transfer: () => transfer(),
payment: () => payment(),
rfq: () => rfq(),
quote: () => quote(),
lock: () => lock(),
capture: () => capture(),
authorize: () => authorize(),
connect: () => connect(),
settle: () => settle(),
reject: () => reject(),
cancel: () => cancel(),
revert: () => revert(),
},
// DIDComm messages
messages: {
transferMessage: () => transferMessage(),
paymentMessage: () => paymentMessage(),
rfqMessage: () => rfqMessage(),
quoteMessage: () => quoteMessage(),
lockMessage: () => lockMessage(),
captureMessage: () => captureMessage(),
authorizeMessage: () => authorizeMessage(),
connectMessage: () => connectMessage(),
settleMessage: () => settleMessage(),
rejectMessage: () => rejectMessage(),
cancelMessage: () => cancelMessage(),
revertMessage: () => revertMessage(),
tapMessage: () => tapMessage(),
},
} as const;