Skip to content

Commit

Permalink
Merge pull request #48 from XYOracleNetwork/feature/escrow-helpers-util
Browse files Browse the repository at this point in the history
Escrow Secret Validation Helpers
  • Loading branch information
JoelBCarter authored Sep 20, 2024
2 parents bcdcc67 + a473cd1 commit 20441c7
Show file tree
Hide file tree
Showing 33 changed files with 242 additions and 519 deletions.
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@
"--run",
"--inspect-brk",
"--no-file-parallelism",
"packages/payloadset/packages/payments/src/Discount/lib/spec/findUnfulfilledConditions.spec.ts"
"packages/payload/packages/payments/src/Escrow/validators/common/SecretValidators/spec/getPartySecretSignedValidator.spec.ts"
],
"sourceMaps": true,
"resolveSourceMapLocations": [
Expand Down
2 changes: 2 additions & 0 deletions packages/payload/packages/payments/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@
"devDependencies": {
"@xylabs/ts-scripts-yarn3": "^4.0.7",
"@xylabs/tsconfig": "^4.0.7",
"@xyo-network/account": "^3.1.11",
"@xyo-network/payload-builder": "^3.1.11",
"typescript": "^5.5.4"
},
"publishConfig": {
Expand Down
6 changes: 6 additions & 0 deletions packages/payload/packages/payments/src/Escrow/Terms/Party.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { EscrowTermsFields } from './Terms.ts'

/**
* The party in an escrow transaction
*/
export type EscrowParty = keyof Pick<EscrowTermsFields, 'buyer' | 'seller'>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { EscrowTermsFields } from './Terms.ts'

/**
* The party's secret in an escrow transaction
*/
export type EscrowPartySecret = keyof Pick<EscrowTermsFields, 'buyerSecret' | 'sellerSecret'>
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
isPayloadOfSchemaType, isPayloadOfSchemaTypeWithMeta, isPayloadOfSchemaTypeWithSources,
} from '@xyo-network/payload-model'

import { EscrowSchema } from './Schema.ts'
import { EscrowSchema } from '../Schema.ts'

export const EscrowTermsSchema = `${EscrowSchema}.terms` as const
export type EscrowTermsSchema = typeof EscrowTermsSchema
Expand Down
3 changes: 3 additions & 0 deletions packages/payload/packages/payments/src/Escrow/Terms/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './Party.ts'
export * from './PartySecret.ts'
export * from './Terms.ts'
5 changes: 2 additions & 3 deletions packages/payload/packages/payments/src/Escrow/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
export * from './createEscrowIntent.ts'
export * from './getEscrowSecret.ts'
export * from './Outcome.ts'
export * from './Schema.ts'
export * from './Terms.ts'
export * from './Terms/index.ts'
export * from './util/index.ts'
export * from './validators/index.ts'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './secret/index.ts'
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { AccountInstance } from '@xyo-network/account-model'
import { BoundWitnessBuilder } from '@xyo-network/boundwitness-builder'
import type { IdPayload } from '@xyo-network/id-payload-plugin'

import type { EscrowTerms } from './Terms.ts'
import type { EscrowTerms } from '../../Terms/index.ts'

/**
* Creates an escrow intent
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { Hash } from '@xylabs/hex'
import type { BoundWitness } from '@xyo-network/boundwitness-model'
import { isBoundWitnessWithMeta } from '@xyo-network/boundwitness-model'
import type { Payload, WithMeta } from '@xyo-network/payload-model'

import type {
EscrowParty, EscrowPartySecret, EscrowTerms,
} from '../../Terms/index.ts'
/**
* Returns the log prefix for the party
* @param party The party
* @returns The log prefix for the party
*/
const getLogPrefix = (party: EscrowParty) => {
const partySecret: EscrowPartySecret = party === 'seller' ? 'sellerSecret' : 'buyerSecret'
return `EscrowTerms.${partySecret}`
}

/**
* Returns an array of BoundWitnesses containing the secret signed by all the parties
* @param terms The escrow terms
* @param dictionary A dictionary of all the payloads associated with the escrow
* @param party The party to get the secret signatures for
* @returns An array of BoundWitnesses containing the secret signed by all the parties
*/
export const findEscrowPartySecretSignatures = (terms: EscrowTerms, dictionary: Record<Hash, WithMeta<Payload>>, party: EscrowParty): BoundWitness[] => {
const partyAddresses = terms[party]
if (partyAddresses === undefined || partyAddresses.length === 0) {
console.log(`${getLogPrefix(party)}: No ${party}: ${terms[party]}`)
return []
}
const partySecret: EscrowPartySecret = party === 'seller' ? 'sellerSecret' : 'buyerSecret'
const secretHash = terms[partySecret]
if (secretHash === undefined) {
console.log(`${getLogPrefix(party)}: No ${partySecret}: ${terms[partySecret]}`)
return []
}
// BWs containing the secret signed by all the parties
const partySignedBWs = Object.values(dictionary)
// Find all BoundWitnesses
.filter(isBoundWitnessWithMeta)
// That contain the seller secret
.filter(bw => bw.payload_hashes.includes(secretHash))
// That are signed by all the parties
.filter(bw => partyAddresses.every(address => bw.addresses.includes(address)))
return partySignedBWs
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './createEscrowIntent.ts'
export * from './findEscrowPartySecretSignatures.ts'
export * from './getEscrowSecret.ts'
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ModuleIdentifier } from '@xyo-network/module-model'
import type { PayloadValueExpression } from '@xyo-network/payload-model'

import type { EscrowTerms } from '../../../Terms.ts'
import type { EscrowTerms } from '../../../Terms/index.ts'

/**
* Checks if property value of the escrow terms contains one of the valid moduleIdentifiers
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { Address } from '@xylabs/hex'
import type { ModuleIdentifier } from '@xyo-network/module-model'

import type { EscrowTerms } from '../../../../Terms'
import { EscrowTermsSchema } from '../../../../Terms'
import { moduleIdentifiersContainsOneOf } from '../moduleInstanceValidators'
import type { EscrowTerms } from '../../../../Terms/index.ts'
import { EscrowTermsSchema } from '../../../../Terms/index.ts'
import { moduleIdentifiersContainsOneOf } from '../moduleInstanceValidators.ts'

describe('RegistrarSentinel', () => {
const appraisalAuthority1: Address = 'address1'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { Hash } from '@xylabs/hex'
import { BoundWitnessValidator } from '@xyo-network/boundwitness-validator'
import type {
AsyncPayloadValidationFunction, Payload, WithMeta,
} from '@xyo-network/payload-model'

import type {
EscrowParty, EscrowPartySecret, EscrowTerms,
} from '../../../Terms/index.ts'
import { findEscrowPartySecretSignatures } from '../../../util/index.ts'

/**
* Returns the log prefix for the party
* @param party The party
* @returns The log prefix for the party
*/
const getLogPrefix = (party: EscrowParty) => {
const partySecret: EscrowPartySecret = party === 'seller' ? 'sellerSecret' : 'buyerSecret'
return `EscrowTerms.${partySecret}`
}

/**
* Returns a function that validates the escrow terms for the existence of the party secret signed by the party
* @param dictionary Payload dictionary of the escrow terms
* @returns A function that validates the escrow terms for the existence of the party secret signed by the party
*/
export const getPartySecretSignedValidator = (dictionary: Record<Hash, WithMeta<Payload>>, party: EscrowParty): AsyncPayloadValidationFunction<EscrowTerms> => {
const partySecret: EscrowPartySecret = party === 'seller' ? 'sellerSecret' : 'buyerSecret'
return async (terms: EscrowTerms): Promise<boolean> => {
// Party-signed party secret BWs
const buyerSecretBWs = findEscrowPartySecretSignatures(terms, dictionary, party)

// If there are no BWs, return false
if (buyerSecretBWs.length === 0) {
console.log(`${getLogPrefix(party)}: No BoundWitnesses supplied for ${partySecret}: ${terms[partySecret]}`)
return false
}

// Ensure each BW supplied for the party secret is valid
const errors = await Promise.all(buyerSecretBWs.map(bw => new BoundWitnessValidator(bw).validate()))
const validBoundWitnesses = errors.every(errors => errors.length === 0)
if (!validBoundWitnesses) {
console.log(`${getLogPrefix(party)}: Invalid BoundWitnesses supplied for ${partySecret}: ${terms[partySecret]}`)
return false
}
return true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './getPartySecretSignedValidator.ts'
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/* eslint-disable max-nested-callbacks */
import { HDWallet } from '@xyo-network/account'
import type { AccountInstance } from '@xyo-network/account-model'
import type { BoundWitness } from '@xyo-network/boundwitness-model'
import type { Id } from '@xyo-network/id-payload-plugin'
import { IdSchema } from '@xyo-network/id-payload-plugin'
import { PayloadBuilder } from '@xyo-network/payload-builder'
import type { Payload } from '@xyo-network/payload-model'

import type { EscrowParty, EscrowTerms } from '../../../../Terms/index.ts'
import { EscrowTermsSchema } from '../../../../Terms/index.ts'
import { createEscrowIntentWithSecret } from '../../../../util/index.ts'
import { getPartySecretSignedValidator } from '../getPartySecretSignedValidator.ts'

describe('RegistrarSentinel', () => {
let buyer: AccountInstance
let seller: AccountInstance
const baseTerms: EscrowTerms = { schema: EscrowTermsSchema }
const buyerSecret: Id = { schema: IdSchema, salt: `${Date.now() - 10}` }
let buyerSecretSignature: BoundWitness
const sellerSecret: Id = { schema: IdSchema, salt: `${Date.now() + 10}` }
let sellerSecretSignature: BoundWitness

describe('getPartySecretSignedValidator', () => {
beforeAll(async () => {
buyer = await HDWallet.random()
seller = await HDWallet.random()

baseTerms.buyer = [buyer.address]
baseTerms.buyerSecret = await PayloadBuilder.dataHash(buyerSecret)
baseTerms.seller = [seller.address]
baseTerms.sellerSecret = await PayloadBuilder.dataHash(sellerSecret)

const buyerIntent = await createEscrowIntentWithSecret(baseTerms, buyerSecret, buyer)
const sellerIntent = await createEscrowIntentWithSecret(baseTerms, sellerSecret, seller)

buyerSecretSignature = buyerIntent[0]
sellerSecretSignature = sellerIntent[0]
})
const cases: EscrowParty[] = ['buyer', 'seller']
describe.each(cases)('for %s', (party) => {
describe('returns true', () => {
it('with valid escrow terms and values supplied', async () => {
const payloads = party === 'buyer' ? [baseTerms, buyerSecret, buyerSecretSignature] : [baseTerms, sellerSecret, sellerSecretSignature]
const dictionary = await PayloadBuilder.toAllHashMap(payloads)
const partySecretValidator = getPartySecretSignedValidator(dictionary, party)
const result = await partySecretValidator(baseTerms)
expect(result).toBeTrue()
})
})
describe('returns false', () => {
describe('with invalid escrow terms value for', () => {
describe('secret', () => {
it('undefined', async () => {
const terms = { ...baseTerms, [`${party}Secret`]: undefined }
const payloads = party === 'buyer' ? [terms, buyerSecret, buyerSecretSignature] : [terms, sellerSecret, sellerSecretSignature]
const dictionary = await PayloadBuilder.toAllHashMap(payloads)
const partySecretValidator = getPartySecretSignedValidator(dictionary, party)
const result = await partySecretValidator(terms)
expect(result).toBeFalse()
})
it('different from signature', async () => {
const secret: Id = { schema: IdSchema, salt: '0' }
const terms = { ...baseTerms, [`${party}Secret`]: await PayloadBuilder.dataHash(secret) }
const payloads = party === 'buyer' ? [terms, buyerSecret, buyerSecretSignature] : [terms, sellerSecret, sellerSecretSignature]
const dictionary = await PayloadBuilder.toAllHashMap(payloads)
const partySecretValidator = getPartySecretSignedValidator(dictionary, party)
const result = await partySecretValidator(terms)
expect(result).toBeFalse()
})
})
})
describe('with missing value for', () => {
it('boundwitness', async () => {
const payloads: Payload[] = party === 'buyer' ? [baseTerms, buyerSecret] : [baseTerms, sellerSecret]
const dictionary = await PayloadBuilder.toAllHashMap(payloads)
const partySecretValidator = getPartySecretSignedValidator(dictionary, party)
const result = await partySecretValidator(baseTerms)
expect(result).toBeFalse()
})
})
})
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "NodeNext",
"module": "NodeNext",
"sourceMap": true,
"inlineSources": true
},
"extends": "@xylabs/tsconfig-jest"
}
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './ModuleInstanceValidators/index.ts'
export * from './SecretValidators/index.ts'
export * from './TemporalValidators/index.ts'
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ModuleIdentifier } from '@xyo-network/module-model'
import type { SyncPayloadValidationFunction } from '@xyo-network/payload-model'

import type { EscrowTerms } from '../../Terms.ts'
import type { EscrowTerms } from '../../Terms/index.ts'
import { moduleIdentifiersContainsOneOf } from '../common/index.ts'

const name = 'EscrowTerms.escrowAgent'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type {
Payload, SyncPayloadValidationFunction, WithMeta, WithSources,
} from '@xyo-network/payload-model'

import type { EscrowTerms } from '../../Terms.ts'
import type { EscrowTerms } from '../../Terms/index.ts'
import { validateWithinWindow } from '../common/index.ts'

const name = 'EscrowTerms.appraisal'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { asAddress } from '@xylabs/hex'
import type { ModuleIdentifier } from '@xyo-network/module-model'
import type { SyncPayloadValidationFunction } from '@xyo-network/payload-model'

import type { EscrowTerms } from '../../Terms.ts'
import type { EscrowTerms } from '../../Terms/index.ts'
import { moduleIdentifiersContainsAllOf } from '../common/index.ts'

const name = 'EscrowTerms.appraisalAuthorities'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { SyncPayloadValidationFunction } from '@xyo-network/payload-model'

import type { EscrowTerms } from '../../Terms.ts'
import type { EscrowTerms } from '../../Terms/index.ts'

const name = 'EscrowTerms.assets'

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { asAddress } from '@xylabs/hex'
import type { SyncPayloadValidationFunction } from '@xyo-network/payload-model'

import type { EscrowTerms } from '../../Terms.ts'
import type { EscrowTerms } from '../../Terms/index.ts'

const name = 'EscrowTerms.buyer'

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { assertEx } from '@xylabs/assert'
import type { Hash } from '@xylabs/hex'
import { isBoundWitnessWithMeta } from '@xyo-network/boundwitness-model'
import { BoundWitnessValidator } from '@xyo-network/boundwitness-validator'
import type {
AsyncPayloadValidationFunction, Payload, SyncPayloadValidationFunction, WithMeta,
} from '@xyo-network/payload-model'

import type { EscrowTerms } from '../../Terms.ts'
import type { EscrowTerms } from '../../Terms/index.ts'
import { getPartySecretSignedValidator } from '../common/index.ts'

const name = 'EscrowTerms.buyerSecret'

Expand Down Expand Up @@ -46,31 +45,5 @@ export const getBuyerSecretSuppliedValidator = (dictionary: Record<Hash, WithMet
* @returns A function that validates the escrow terms for the existence of the buyerSecret signed by the buyer
*/
export const getBuyerSecretSignedValidator = (dictionary: Record<Hash, WithMeta<Payload>>): AsyncPayloadValidationFunction<EscrowTerms> => {
return async (terms: EscrowTerms): Promise<boolean> => {
const buyer = assertEx(terms.buyer, () => `${name}: No buyer: ${terms.buyer}`)
const buyerSecret = assertEx(terms.buyerSecret, () => `${name}: No buyerSecret: ${terms.buyerSecret}`)
// Buyer-signed buyer secrets
const buyerSecretBWs = Object.values(dictionary)
// Find all BoundWitnesses
.filter(isBoundWitnessWithMeta)
// That contain the buyer secret
.filter(bw => bw.payload_hashes.includes(buyerSecret))
// That are signed by all the buyers
.filter(bw => buyer.every(buyerAddress => bw.addresses.includes(buyerAddress)))

// If there are no buyerSecret BWs, return false
if (buyerSecretBWs.length === 0) {
console.log(`${name}: No BoundWitnesses supplied for buyerSecret: ${buyerSecret}`)
return false
}

// Ensure each BW supplied for the buyerSecret is valid
const errors = await Promise.all(buyerSecretBWs.map(bw => new BoundWitnessValidator(bw).validate()))
const validBoundWitnesses = errors.every(errors => errors.length === 0)
if (!validBoundWitnesses) {
console.log(`${name}: Invalid BoundWitnesses supplied for buyerSecret: ${buyerSecret}`)
return false
}
return true
}
return getPartySecretSignedValidator(dictionary, 'buyer')
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { SyncPayloadValidationFunction } from '@xyo-network/payload-model'

import type { EscrowTerms } from '../../Terms.ts'
import type { EscrowTerms } from '../../Terms/index.ts'

export const getNbfExpValidator = (now: number, minRequiredDuration: number): SyncPayloadValidationFunction<EscrowTerms> => {
const minExp = now + minRequiredDuration
Expand Down
Loading

0 comments on commit 20441c7

Please sign in to comment.